Skip to content

Commit 6a9842c

Browse files
gijzelaerrclaude
andcommitted
Add s7 unified client/server tests, fix mypy issues
29 new tests for s7.Client and s7.Server using the built-in server emulator (no real PLC needed): - Legacy protocol: connect, db_read, db_write, db_read_multi, list_datablocks, context manager, repr, delegated methods, diagnostic buffer, attribute errors - S7CommPlus protocol: connect, db_read, db_write, db_read_multi, explore, auto-detection - S7CommPlus guards: browse/explore/subscription require S7CommPlus - Server: context manager, register_db, register_raw_db, get_db, property accessors - Protocol enum values Bug fix in s7/client.py: legacy connection is now optional when S7CommPlus is explicitly requested — PLCs with PUT/GET disabled and test emulators don't support both protocols on the same port. Coverage improvements: - s7/client.py: 21% -> 87% - s7/server.py: 43% -> 84% Also fixed mypy issues in test_logging.py. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 701cdd6 commit 6a9842c

3 files changed

Lines changed: 360 additions & 11 deletions

File tree

s7/client.py

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -127,10 +127,23 @@ def connect(
127127
else:
128128
self._protocol = Protocol.LEGACY
129129

130-
# Always connect legacy client (needed for block ops, PLC control, etc.)
131-
self._legacy = LegacyClient()
132-
self._legacy.connect(address, rack, slot, tcp_port)
133-
logger.info(f"Legacy S7 connected to {address}:{tcp_port}")
130+
# Connect legacy client for block ops, PLC control, etc.
131+
# Skip when S7CommPlus was explicitly requested — the target may not
132+
# support legacy S7 (e.g. PUT/GET disabled) or use a different port
133+
# (e.g. test emulators).
134+
if self._protocol != Protocol.S7COMMPLUS:
135+
self._legacy = LegacyClient()
136+
self._legacy.connect(address, rack, slot, tcp_port)
137+
logger.info(f"Legacy S7 connected to {address}:{tcp_port}")
138+
elif protocol == Protocol.AUTO:
139+
# AUTO mode with S7CommPlus: also try legacy for block ops
140+
try:
141+
self._legacy = LegacyClient()
142+
self._legacy.connect(address, rack, slot, tcp_port)
143+
logger.info(f"Legacy S7 connected to {address}:{tcp_port}")
144+
except Exception as e:
145+
logger.debug(f"Legacy S7 connection failed (S7CommPlus available): {e}")
146+
self._legacy = None
134147

135148
return self
136149

tests/test_logging.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,20 @@
33
import json
44
import logging
55

6+
import pytest
7+
68
from snap7.log import PLCLoggerAdapter, OperationLogger, JSONFormatter
79

810

911
class TestPLCLoggerAdapter:
10-
def test_prefix_added(self, caplog: logging.LogRecord) -> None: # type: ignore[type-arg]
12+
def test_prefix_added(self, caplog: pytest.LogCaptureFixture) -> None:
1113
base = logging.getLogger("test.adapter")
1214
adapter = PLCLoggerAdapter(base, plc_host="10.0.0.1", rack=0, slot=1)
1315
with caplog.at_level(logging.INFO, logger="test.adapter"):
1416
adapter.info("Connected")
1517
assert "[10.0.0.1 R0/S1] Connected" in caplog.text
1618

17-
def test_no_prefix_without_host(self, caplog: logging.LogRecord) -> None: # type: ignore[type-arg]
19+
def test_no_prefix_without_host(self, caplog: pytest.LogCaptureFixture) -> None:
1820
base = logging.getLogger("test.nohost")
1921
adapter = PLCLoggerAdapter(base)
2022
with caplog.at_level(logging.INFO, logger="test.nohost"):
@@ -40,7 +42,7 @@ def test_update_context_partial(self) -> None:
4042

4143

4244
class TestOperationLogger:
43-
def test_logs_timing(self, caplog: logging.LogRecord) -> None: # type: ignore[type-arg]
45+
def test_logs_timing(self, caplog: pytest.LogCaptureFixture) -> None:
4446
base = logging.getLogger("test.oplog")
4547
with caplog.at_level(logging.DEBUG, logger="test.oplog"):
4648
with OperationLogger(base, "db_read", db=1, start=0, size=4):
@@ -49,7 +51,7 @@ def test_logs_timing(self, caplog: logging.LogRecord) -> None: # type: ignore[t
4951
assert "db=1" in caplog.text
5052
assert "ms)" in caplog.text
5153

52-
def test_works_with_adapter(self, caplog: logging.LogRecord) -> None: # type: ignore[type-arg]
54+
def test_works_with_adapter(self, caplog: pytest.LogCaptureFixture) -> None:
5355
base = logging.getLogger("test.oplog_adapter")
5456
adapter = PLCLoggerAdapter(base, plc_host="10.0.0.1", rack=0, slot=1)
5557
with caplog.at_level(logging.DEBUG, logger="test.oplog_adapter"):
@@ -90,9 +92,9 @@ def test_plc_context_included(self) -> None:
9092
args=None,
9193
exc_info=None,
9294
)
93-
record.plc_host = "192.168.1.10" # type: ignore[attr-defined]
94-
record.plc_rack = 0 # type: ignore[attr-defined]
95-
record.plc_slot = 1 # type: ignore[attr-defined]
95+
record.plc_host = "192.168.1.10"
96+
record.plc_rack = 0
97+
record.plc_slot = 1
9698
output = formatter.format(record)
9799
data = json.loads(output)
98100
assert data["plc_host"] == "192.168.1.10"

0 commit comments

Comments
 (0)