"""Reconnexion automatique de SessionTabWidget : programmation du delai (avec backoff), plafond de tentatives, remise a zero apres succes, et absence de reconnexion sur une fermeture volontaire de l'onglet. Pilote directement _on_disconnected()/_on_connected() plutot que d'attendre une vraie coupure reseau (la detection elle-meme est testee dans test_keepalive.py). SSHConnection.start() est neutralisee : ce module teste uniquement la logique de programmation du cote de l'onglet, qui n'a besoin d'aucune connexion reseau reelle - cela evite aussi qu'un thread reste bloque dans un connect() lent et ne bloque la fin du script. """ import pathlib import sys sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent)) import time from unittest.mock import patch from PySide6.QtWidgets import QApplication from app.session_manager import Session from app.ssh_connection import SSHConnection from app.ui.tabs import SessionTabWidget app = QApplication.instance() or QApplication([]) def pump(seconds: float) -> None: deadline = time.time() + seconds while time.time() < deadline: app.processEvents() time.sleep(0.01) def make_session(**overrides) -> Session: session = Session(name="t", host="10.0.0.1", port=22, username="root") for key, value in overrides.items(): setattr(session, key, value) return session no_real_thread = patch.object(SSHConnection, "start", lambda self: None) no_real_thread.start() print("=== 1. Reconnexion desactivee par defaut : aucune programmation ===") session = make_session() assert session.reconnect_enabled is False tab = SessionTabWidget(session, "") tab._on_disconnected("test") assert not tab._reconnect_timer.isActive() tab.close_connection() print(" OK") print() print("=== 2. Reconnexion activee : programmation avec le delai initial exact ===") session2 = make_session(reconnect_enabled=True, reconnect_initial_delay=1, reconnect_max_attempts=3) tab2 = SessionTabWidget(session2, "") tab2._on_disconnected("connexion perdue") print(" statut:", tab2.status_label.text()) assert tab2._reconnect_timer.isActive() assert tab2._reconnect_attempt == 1 assert tab2._reconnect_seconds_left == 1 # compute_backoff_delay(1, 1) == 1 assert "Tentative 1/3" in tab2.status_label.text() print(" OK") print() print("=== 3. Le timer se declenche reellement et relance une connexion ===") pump(1.5) print(" countdown actif apres ~1.5s:", tab2._reconnect_countdown_timer.isActive()) assert not tab2._reconnect_countdown_timer.isActive(), "le cycle doit s'etre declenche et avoir tout arrete" tab2.close_connection() print(" OK") print() print("=== 4. Plafond de tentatives : abandon au-dela de max_attempts ===") session3 = make_session(reconnect_enabled=True, reconnect_initial_delay=100, reconnect_max_attempts=2) tab3 = SessionTabWidget(session3, "") tab3._on_disconnected("r1") assert tab3._reconnect_attempt == 1 and tab3._reconnect_timer.isActive() tab3._cancel_reconnect_timers() # simule le declenchement du timer sans attendre 100s tab3._on_disconnected("r2") assert tab3._reconnect_attempt == 2 and tab3._reconnect_timer.isActive() tab3._cancel_reconnect_timers() tab3._on_disconnected("r3") print(" apres 3 echecs (max=2):", tab3.status_label.text()) assert tab3._reconnect_attempt == 3 assert not tab3._reconnect_timer.isActive(), "au-dela du plafond, plus de nouvelle tentative programmee" assert "Abandon" in tab3.status_label.text() tab3.close_connection() print(" OK") print() print("=== 5. Une connexion reussie remet le compteur de tentatives a zero ===") session4 = make_session(reconnect_enabled=True) tab4 = SessionTabWidget(session4, "") tab4._reconnect_attempt = 5 tab4._on_connected() assert tab4._reconnect_attempt == 0 tab4.close_connection() print(" OK") print() print("=== 6. Fermeture volontaire de l'onglet : jamais de reconnexion programmee ===") session5 = make_session(reconnect_enabled=True) tab5 = SessionTabWidget(session5, "") tab5.close_connection() # fermeture "pour de vrai" -> _closing = True tab5._on_disconnected("apres fermeture") assert not tab5._reconnect_timer.isActive() print(" OK") print() print("=== 7. Reconnexion manuelle (bouton) annule un compte a rebours en cours ===") session6 = make_session(reconnect_enabled=True, reconnect_initial_delay=100) tab6 = SessionTabWidget(session6, "") tab6._on_disconnected("r1") assert tab6._reconnect_timer.isActive() tab6.reconnect() assert not tab6._reconnect_timer.isActive() assert not tab6._reconnect_countdown_timer.isActive() tab6.close_connection() print(" OK") no_real_thread.stop() print() print("RECONNEXION AUTOMATIQUE : TOUS LES TESTS OK")