From f71d42894733f3f6d68374887036d7c718497668 Mon Sep 17 00:00:00 2001 From: Yash Anand <164618230+YashAnand69@users.noreply.github.com> Date: Fri, 9 Oct 2026 16:36:32 +0530 Subject: [PATCH 1/2] fix: preserve WLAN timeout across reconnects --- pslab/connection/wlan.py | 1 + tests/test_wlan_timeout.py | 127 +++++++++++++++++++++++++++++++++++++ 2 files changed, 128 insertions(+) create mode 100644 tests/test_wlan_timeout.py diff --git a/pslab/connection/wlan.py b/pslab/connection/wlan.py index 79cde7a..ad26c89 100644 --- a/pslab/connection/wlan.py +++ b/pslab/connection/wlan.py @@ -46,6 +46,7 @@ def timeout(self) -> float: @timeout.setter def timeout(self, value: float) -> None: self._sock.settimeout(value) + self._timeout = value def connect(self) -> None: """Connect to PSLab.""" diff --git a/tests/test_wlan_timeout.py b/tests/test_wlan_timeout.py new file mode 100644 index 0000000..cf93481 --- /dev/null +++ b/tests/test_wlan_timeout.py @@ -0,0 +1,127 @@ +"""WLAN timeout configuration tests without a physical PSLab device.""" + +import socket + +import pytest + +from pslab.connection.wlan import WLANHandler + + +@pytest.mark.parametrize("timeout", [0.25, 2.0, None]) +def test_timeout_update_survives_reconnect(monkeypatch, timeout): + """Keep the requested timeout on the current and replacement sockets.""" + monkeypatch.setattr(WLANHandler, "get_version", lambda self: "PSLab V6") + + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server: + server.bind(("127.0.0.1", 0)) + server.listen() + handler = WLANHandler(host="127.0.0.1", port=server.getsockname()[1]) + + try: + handler.timeout = timeout + assert handler._sock.gettimeout() == timeout + + handler.connect() + connection, _ = server.accept() + connection.close() + handler.disconnect() + + handler.connect() + connection, _ = server.accept() + connection.close() + assert handler._sock.gettimeout() == timeout + assert handler.timeout == timeout + finally: + handler.disconnect() + + +@pytest.mark.parametrize("timeout", [0.25, 2.0, None]) +def test_timeout_property_tracks_update(timeout): + """Report the updated value consistently with the live socket.""" + handler = WLANHandler() + + try: + handler.timeout = timeout + assert handler._sock.gettimeout() == timeout + assert handler.timeout == timeout + finally: + handler.disconnect() + + +@pytest.mark.parametrize("timeout, error", [(-1, ValueError), ("invalid", TypeError)]) +def test_invalid_timeout_preserves_previous_value(timeout, error): + """Reject an invalid value without changing the configured timeout.""" + handler = WLANHandler(timeout=0.5) + + try: + with pytest.raises(error): + handler.timeout = timeout + + assert handler.timeout == 0.5 + assert handler._sock.gettimeout() == 0.5 + finally: + handler.disconnect() + + +def test_initial_timeout_matches_socket(): + """Expose the configured constructor timeout without an update.""" + handler = WLANHandler(timeout=0.5) + + try: + assert handler.timeout == 0.5 + assert handler._sock.gettimeout() == 0.5 + finally: + handler.disconnect() + + +def test_timeout_update_preserves_transfers(monkeypatch): + """Keep normal transfers working after changing the timeout.""" + monkeypatch.setattr(WLANHandler, "get_version", lambda self: "PSLab V6") + + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server: + server.bind(("127.0.0.1", 0)) + server.listen() + handler = WLANHandler(host="127.0.0.1", port=server.getsockname()[1]) + + try: + handler.timeout = 0.5 + handler.connect() + connection, _ = server.accept() + + with connection: + connection.settimeout(0.5) + assert handler.write(b"PSLab") == 5 + with connection.makefile("rb") as response: + assert response.read(5) == b"PSLab" + connection.sendall(b"OK") + assert handler.read(2) == b"OK" + assert handler.write(b"") == 0 + assert handler.read(0) == b"" + + assert repr(handler) == f"WLANHandler[127.0.0.1:{handler.port}]" + finally: + handler.disconnect() + + +def test_failed_handshake_closes_socket(monkeypatch): + """Preserve socket cleanup when the device handshake fails.""" + + def fail_handshake(self): + raise ConnectionError("Not a PSLab device") + + monkeypatch.setattr(WLANHandler, "get_version", fail_handshake) + + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server: + server.bind(("127.0.0.1", 0)) + server.listen() + handler = WLANHandler(host="127.0.0.1", port=server.getsockname()[1]) + + try: + with pytest.raises(ConnectionError, match="Not a PSLab device"): + handler.connect() + + connection, _ = server.accept() + connection.close() + assert handler._sock.fileno() == -1 + finally: + handler.disconnect() From f9481e7e96ef46c47b63f2702039eb927ac7c51a Mon Sep 17 00:00:00 2001 From: Yash Anand <164618230+YashAnand69@users.noreply.github.com> Date: Fri, 9 Oct 2026 16:40:54 +0530 Subject: [PATCH 2/2] fix: annotate optional WLAN timeouts --- pslab/connection/wlan.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/pslab/connection/wlan.py b/pslab/connection/wlan.py index ad26c89..9dd9caa 100644 --- a/pslab/connection/wlan.py +++ b/pslab/connection/wlan.py @@ -1,6 +1,7 @@ """Wireless interface for communicating with PSLab devices equiped with ESP8266.""" import socket +from typing import Optional from pslab.connection.connection import ConnectionHandler @@ -13,14 +14,15 @@ class WLANHandler(ConnectionHandler): host : str, default 192.168.4.1 Network address of the PSLab. port : int, default 80 - timeout : float, default 1 s + timeout : float or None, default 1 s + Use None to disable the socket timeout. """ def __init__( self, host: str = "192.168.4.1", port: int = 80, - timeout: float = 1.0, + timeout: Optional[float] = 1.0, ) -> None: self._host = host self._port = port @@ -39,12 +41,12 @@ def port(self) -> int: return self._port @property - def timeout(self) -> float: + def timeout(self) -> Optional[float]: """Timeout in seconds.""" return self._timeout @timeout.setter - def timeout(self, value: float) -> None: + def timeout(self, value: Optional[float]) -> None: self._sock.settimeout(value) self._timeout = value