Skip to content

Commit 4bded55

Browse files
gijzelaerrclaude
andauthored
Add tests for snap7/logo.py to improve coverage from 53% to 97% (gijzelaerr#643)
Tests parse_address() for all address types (V, VW, VD, V.bit) and invalid inputs, plus integration tests for Logo read/write against the built-in server covering byte, word, dword, and bit operations including boundary values and read-modify-write bit logic. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 6ccacf5 commit 4bded55

File tree

1 file changed

+260
-0
lines changed

1 file changed

+260
-0
lines changed

tests/test_logo_coverage.py

Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
"""Tests for snap7/logo.py to improve coverage of parse_address, read, and write."""
2+
3+
import logging
4+
import unittest
5+
from typing import Optional
6+
7+
import pytest
8+
9+
from snap7.logo import Logo, parse_address
10+
from snap7.server import Server
11+
from snap7.type import SrvArea, WordLen
12+
13+
logging.basicConfig(level=logging.WARNING)
14+
15+
ip = "127.0.0.1"
16+
tcpport = 11102
17+
db_number = 1
18+
19+
20+
# ---------------------------------------------------------------------------
21+
# parse_address() unit tests (no server needed)
22+
# ---------------------------------------------------------------------------
23+
24+
25+
@pytest.mark.logo
26+
class TestParseAddress(unittest.TestCase):
27+
"""Test every branch of parse_address()."""
28+
29+
def test_byte_address(self) -> None:
30+
start, wl = parse_address("V10")
31+
self.assertEqual(start, 10)
32+
self.assertEqual(wl, WordLen.Byte)
33+
34+
def test_byte_address_large(self) -> None:
35+
start, wl = parse_address("V999")
36+
self.assertEqual(start, 999)
37+
self.assertEqual(wl, WordLen.Byte)
38+
39+
def test_word_address(self) -> None:
40+
start, wl = parse_address("VW20")
41+
self.assertEqual(start, 20)
42+
self.assertEqual(wl, WordLen.Word)
43+
44+
def test_word_address_zero(self) -> None:
45+
start, wl = parse_address("VW0")
46+
self.assertEqual(start, 0)
47+
self.assertEqual(wl, WordLen.Word)
48+
49+
def test_dword_address(self) -> None:
50+
start, wl = parse_address("VD30")
51+
self.assertEqual(start, 30)
52+
self.assertEqual(wl, WordLen.DWord)
53+
54+
def test_bit_address(self) -> None:
55+
start, wl = parse_address("V10.3")
56+
# bit offset = 10*8 + 3 = 83
57+
self.assertEqual(start, 83)
58+
self.assertEqual(wl, WordLen.Bit)
59+
60+
def test_bit_address_zero(self) -> None:
61+
start, wl = parse_address("V0.0")
62+
self.assertEqual(start, 0)
63+
self.assertEqual(wl, WordLen.Bit)
64+
65+
def test_bit_address_high_bit(self) -> None:
66+
start, wl = parse_address("V0.7")
67+
self.assertEqual(start, 7)
68+
self.assertEqual(wl, WordLen.Bit)
69+
70+
def test_invalid_address_raises(self) -> None:
71+
with self.assertRaises(ValueError):
72+
parse_address("INVALID")
73+
74+
def test_invalid_address_empty(self) -> None:
75+
with self.assertRaises(ValueError):
76+
parse_address("")
77+
78+
def test_invalid_address_wrong_prefix(self) -> None:
79+
with self.assertRaises(ValueError):
80+
parse_address("M10")
81+
82+
83+
# ---------------------------------------------------------------------------
84+
# Integration tests: Logo client against the built-in Server
85+
# ---------------------------------------------------------------------------
86+
87+
88+
@pytest.mark.logo
89+
class TestLogoReadWrite(unittest.TestCase):
90+
"""Test Logo read/write against a real server with DB1 registered."""
91+
92+
server: Optional[Server] = None
93+
db_data: bytearray
94+
95+
@classmethod
96+
def setUpClass(cls) -> None:
97+
cls.db_data = bytearray(256)
98+
cls.server = Server()
99+
cls.server.register_area(SrvArea.DB, 0, bytearray(256))
100+
cls.server.register_area(SrvArea.DB, 1, cls.db_data)
101+
cls.server.start(tcp_port=tcpport)
102+
103+
@classmethod
104+
def tearDownClass(cls) -> None:
105+
if cls.server:
106+
cls.server.stop()
107+
cls.server.destroy()
108+
109+
def setUp(self) -> None:
110+
self.client = Logo()
111+
self.client.connect(ip, 0x1000, 0x2000, tcpport)
112+
113+
def tearDown(self) -> None:
114+
self.client.disconnect()
115+
self.client.destroy()
116+
117+
# -- read tests ---------------------------------------------------------
118+
119+
def test_read_byte(self) -> None:
120+
"""Write a known byte into DB1 via client, then read it back."""
121+
self.client.write("V5", 0xAB)
122+
result = self.client.read("V5")
123+
self.assertEqual(result, 0xAB)
124+
125+
def test_read_word(self) -> None:
126+
"""Write and read back a word (signed 16-bit big-endian)."""
127+
self.client.write("VW10", 1234)
128+
result = self.client.read("VW10")
129+
self.assertEqual(result, 1234)
130+
131+
def test_read_word_negative(self) -> None:
132+
"""Words are signed — negative values should round-trip."""
133+
self.client.write("VW12", -500)
134+
result = self.client.read("VW12")
135+
self.assertEqual(result, -500)
136+
137+
def test_read_dword(self) -> None:
138+
"""Write and read back a dword (signed 32-bit big-endian)."""
139+
self.client.write("VD20", 70000)
140+
result = self.client.read("VD20")
141+
self.assertEqual(result, 70000)
142+
143+
def test_read_dword_negative(self) -> None:
144+
"""DWords are signed — negative values should round-trip."""
145+
self.client.write("VD24", -123456)
146+
result = self.client.read("VD24")
147+
self.assertEqual(result, -123456)
148+
149+
def test_read_bit_set(self) -> None:
150+
"""Write bit=1, then read it back."""
151+
self.client.write("V50.2", 1)
152+
result = self.client.read("V50.2")
153+
self.assertEqual(result, 1)
154+
155+
def test_read_bit_clear(self) -> None:
156+
"""Write bit=0, then read it back."""
157+
# First set it so we know we're actually clearing
158+
self.client.write("V51.5", 1)
159+
self.assertEqual(self.client.read("V51.5"), 1)
160+
self.client.write("V51.5", 0)
161+
result = self.client.read("V51.5")
162+
self.assertEqual(result, 0)
163+
164+
def test_read_bit_zero(self) -> None:
165+
"""Read bit 0 of byte 0."""
166+
self.client.write("V60", 0) # clear byte first
167+
self.client.write("V60.0", 1)
168+
self.assertEqual(self.client.read("V60.0"), 1)
169+
# Other bits should be 0
170+
self.assertEqual(self.client.read("V60.1"), 0)
171+
172+
def test_read_bit_seven(self) -> None:
173+
"""Read bit 7 of a byte."""
174+
self.client.write("V61", 0) # clear byte
175+
self.client.write("V61.7", 1)
176+
self.assertEqual(self.client.read("V61.7"), 1)
177+
# Byte should be 0x80
178+
self.assertEqual(self.client.read("V61"), 0x80)
179+
180+
# -- write tests --------------------------------------------------------
181+
182+
def test_write_byte(self) -> None:
183+
"""Write a byte and verify."""
184+
result = self.client.write("V70", 42)
185+
self.assertEqual(result, 0)
186+
self.assertEqual(self.client.read("V70"), 42)
187+
188+
def test_write_word(self) -> None:
189+
"""Write a word and verify."""
190+
result = self.client.write("VW80", 2000)
191+
self.assertEqual(result, 0)
192+
self.assertEqual(self.client.read("VW80"), 2000)
193+
194+
def test_write_dword(self) -> None:
195+
"""Write a dword and verify."""
196+
result = self.client.write("VD90", 100000)
197+
self.assertEqual(result, 0)
198+
self.assertEqual(self.client.read("VD90"), 100000)
199+
200+
def test_write_bit_true(self) -> None:
201+
"""Write a bit to True."""
202+
result = self.client.write("V100.4", 1)
203+
self.assertEqual(result, 0)
204+
self.assertEqual(self.client.read("V100.4"), 1)
205+
206+
def test_write_bit_false(self) -> None:
207+
"""Write a bit to False after setting it."""
208+
self.client.write("V101.6", 1)
209+
result = self.client.write("V101.6", 0)
210+
self.assertEqual(result, 0)
211+
self.assertEqual(self.client.read("V101.6"), 0)
212+
213+
def test_write_bit_preserves_other_bits(self) -> None:
214+
"""Setting one bit should not disturb other bits in the same byte."""
215+
# Write 0xFF to the byte
216+
self.client.write("V110", 0xFF)
217+
# Clear bit 3
218+
self.client.write("V110.3", 0)
219+
# Byte should now be 0xF7 (all bits set except bit 3)
220+
self.assertEqual(self.client.read("V110"), 0xF7)
221+
# Set bit 3 back
222+
self.client.write("V110.3", 1)
223+
self.assertEqual(self.client.read("V110"), 0xFF)
224+
225+
def test_write_byte_boundary_values(self) -> None:
226+
"""Test boundary values: 0 and 255."""
227+
self.client.write("V120", 0)
228+
self.assertEqual(self.client.read("V120"), 0)
229+
self.client.write("V120", 255)
230+
self.assertEqual(self.client.read("V120"), 255)
231+
232+
def test_write_word_boundary_values(self) -> None:
233+
"""Test word boundary values: max positive and max negative."""
234+
self.client.write("VW130", 32767)
235+
self.assertEqual(self.client.read("VW130"), 32767)
236+
self.client.write("VW130", -32768)
237+
self.assertEqual(self.client.read("VW130"), -32768)
238+
239+
def test_write_dword_boundary_values(self) -> None:
240+
"""Test dword boundary values."""
241+
self.client.write("VD140", 2147483647)
242+
self.assertEqual(self.client.read("VD140"), 2147483647)
243+
self.client.write("VD140", -2147483648)
244+
self.assertEqual(self.client.read("VD140"), -2147483648)
245+
246+
def test_read_write_multiple_addresses(self) -> None:
247+
"""Verify different address types can coexist."""
248+
self.client.write("V200", 0x42)
249+
self.client.write("VW202", 1000)
250+
self.client.write("VD204", 50000)
251+
self.client.write("V208.1", 1)
252+
253+
self.assertEqual(self.client.read("V200"), 0x42)
254+
self.assertEqual(self.client.read("VW202"), 1000)
255+
self.assertEqual(self.client.read("VD204"), 50000)
256+
self.assertEqual(self.client.read("V208.1"), 1)
257+
258+
259+
if __name__ == "__main__":
260+
unittest.main()

0 commit comments

Comments
 (0)