diff --git a/ast_monitor/model.py b/ast_monitor/model.py index 42d8609..2a2fc93 100644 --- a/ast_monitor/model.py +++ b/ast_monitor/model.py @@ -1,6 +1,7 @@ import glob import json import os +import socket import socketserver from PyQt6.QtCore import QThread, pyqtSignal @@ -82,6 +83,10 @@ def initialize_GUI(self) -> None: """ QMainWindow.__init__(self, flags=Qt.WindowType.FramelessWindowHint) Ui_MainWindow.__init__(self) + self._cleanup_complete = False + self.timer = None + self.view = None + self.channel = None self.setupUi(self) # Map setting (not working on Raspberry Pi @@ -123,6 +128,51 @@ def initialize_GUI(self) -> None: self.timer.timeout.connect(self.render_data) self.timer.start(250) + def closeEvent(self, event): + """Release resources that Qt WebEngine keeps outside the widget tree.""" + self.cleanup() + super().closeEvent(event) + + def cleanup(self) -> None: + """ + Stop background resources owned by the main window. + + PyQt's WebEngine objects need an explicit deleteLater/processEvents cycle + in tests, otherwise Qt prints profile-release warnings at interpreter exit. + """ + if self._cleanup_complete: + return + self._cleanup_complete = True + + if self.timer is not None: + self.timer.stop() + + if hasattr(self, "server_thread") and self.server_thread: + self.server_thread.stop() + self.server_thread.wait(3000) + + if self.view is not None: + try: + self.view.loadFinished.disconnect(self.on_load_finished) + except (TypeError, RuntimeError): + pass + try: + self.view.stop() + except RuntimeError: + pass + try: + self.view.page().setWebChannel(None) + except RuntimeError: + pass + self.view.close() + self.view.setParent(None) + self.view.deleteLater() + self.view = None + + if self.channel is not None: + self.channel.deleteLater() + self.channel = None + def update_map_component(self, progress: float = None, remaining_distance: float = None, remaining_ascent: float = None, speed: float = None, heartrate: int = None, duration=None, distance=None, @@ -493,6 +543,7 @@ def convert_time_to_hours_minutes_seconds(self, time: int) -> str: return time class CustomTCPServer(socketserver.TCPServer): + allow_reuse_address = True logging = True def __init__(self, server_address, RequestHandlerClass, bind_and_activate=True, logging=True): @@ -511,17 +562,26 @@ def __init__(self, server_port: int, logging=True): self.running = True self.server_port = server_port self.logging = logging + self.httpd = None def run(self): """ Method that runs the HTTP server. """ Handler = CustomHandler - with CustomTCPServer(("", self.server_port), Handler,self.logging) as httpd: + with CustomTCPServer(("", self.server_port), Handler, logging=self.logging) as httpd: + httpd.timeout = 0.2 + self.httpd = httpd self.signal.emit(f"Serving at http://localhost:{self.server_port}") while self.running: httpd.handle_request() + self.httpd = None def stop(self): """Method that stops the HTTP server.""" self.running = False + try: + with socket.create_connection(("127.0.0.1", self.server_port), timeout=0.2): + pass + except OSError: + pass diff --git a/tests/test_gui.py b/tests/test_gui.py index 483bec3..8352a18 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -1,23 +1,36 @@ -import sys,os,random +import os +import random import pytest +from PyQt6.QtCore import QEvent +from PyQt6.QtWidgets import QApplication try: from ast_monitor.model import AST except ModuleNotFoundError: - sys.path.append('../') + import sys + sys.path.append("../") from ast_monitor.model import AST - @pytest.fixture def widget(qtbot): - hr_data = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'sensor_data', 'hr.txt') - gps_data = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'sensor_data', 'gps.txt') - route_data = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'development', 'routes', 'route.json') - random_port = random.randint(8000, 9000) - window = AST(hr_data, gps_data, route_data,server_port=random_port,logging=False) + hr_data = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "sensor_data", "hr.txt") + gps_data = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "sensor_data", "gps.txt") + route_data = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "development", "routes", "route.json") + random_port = random.randint(8000, 9000) + + window = AST(hr_data, gps_data, route_data, server_port=random_port, logging=False) qtbot.addWidget(window) - return window + + yield window + + + window.cleanup() + window.close() + window.deleteLater() + QApplication.processEvents() + QApplication.sendPostedEvents(None, QEvent.Type.DeferredDelete) + qtbot.wait(100) def test_window_title(qtbot,widget): @@ -56,7 +69,6 @@ def test_start_stop_tracking(qtbot,widget): assert widget.widget_start_stop.currentIndex() == 0 # Shows start icon, is stopped widget.server_thread.stop() -@pytest.mark.skip(reason="Skipping this test temporarily") def test_load_training(qtbot,widget): widget.btn_move_right.click() # Move to Training page widget.btn_load_training.click() # Load training