""" Tests für den MQTT-Reconnect-Mechanismus (Issue #105) — der Client hing nach einem Drucker-Reconnect fest, weil zwei unabhängige Fehlerpfade (der Reader- Thread-Keepalive und publish()'s eigener Reconnect-Trigger) unkoordiniert parallel liefen, UND weil der Poll-Loop einen None-Rückgabewert von publish() (statt einer Exception) nie als "Verbindung tot" erkannte. """ import threading import time import pytest from kobrax_client import KobraXClient def _client(**overrides): kwargs = dict( host="192.168.1.100", username="u", password="p", mode_id="20030", device_id="abc123", port=9883, client_id="test", ) kwargs.update(overrides) return KobraXClient(**kwargs) def test_is_connected_false_when_no_socket(): c = _client() assert c.is_connected() is False def test_is_connected_true_when_socket_present(): c = _client() c._sock = object() # any truthy stand-in for a real socket assert c.is_connected() is True def test_reconnect_concurrent_calls_only_run_do_connect_once(): """Two threads calling _reconnect() at the same time must not both run _do_connect() - only one handshake should happen; the second caller waits for the first instead of racing it (Issue #105).""" c = _client() c._running = True do_connect_calls = [] call_lock = threading.Lock() release_event = threading.Event() def fake_do_connect(): with call_lock: do_connect_calls.append(1) # Simulate a slow handshake so the second _reconnect() call has time # to observe the lock as already held. release_event.wait(timeout=2.0) c._sock = object() c._do_connect = fake_do_connect results = [] def run(): results.append(c._reconnect()) t1 = threading.Thread(target=run) t2 = threading.Thread(target=run) t1.start() time.sleep(0.05) # let t1 acquire the lock and enter _do_connect first t2.start() time.sleep(0.1) release_event.set() # let the in-flight handshake finish t1.join(timeout=3) t2.join(timeout=3) assert len(do_connect_calls) == 1 assert results == [True, True] def test_reconnect_second_waiter_returns_after_first_completes(): c = _client() c._running = True def fake_do_connect(): time.sleep(0.1) c._sock = object() c._do_connect = fake_do_connect t1 = threading.Thread(target=c._reconnect) t1.start() time.sleep(0.02) # Second call while the first is still mid-handshake. result = c._reconnect() t1.join(timeout=3) assert result is True assert c._sock is not None