|
| 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