🎉 Initial commit
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
"""End-to-end server tests over real TCP sockets."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
import pytest
|
||||
|
||||
from wago_sim.mbap import build_frame
|
||||
from wago_sim.pdu import (
|
||||
ExceptionResponse,
|
||||
FC_READ_COILS,
|
||||
FC_READ_DISCRETE_INPUTS,
|
||||
FC_READ_HOLDING_REGISTERS,
|
||||
FC_READ_INPUT_REGISTERS,
|
||||
FC_WRITE_MULTIPLE_REGISTERS,
|
||||
FC_WRITE_SINGLE_COIL,
|
||||
FC_WRITE_SINGLE_REGISTER,
|
||||
)
|
||||
|
||||
|
||||
async def _start(app, port=15020):
|
||||
app.server.host, app.server.port = "127.0.0.1", port
|
||||
await app.server.start()
|
||||
await asyncio.sleep(0.05)
|
||||
return port
|
||||
|
||||
|
||||
async def _roundtrip(reader, writer, txn, request_pdu):
|
||||
writer.write(build_frame(txn, 1, request_pdu))
|
||||
await writer.drain()
|
||||
header = await reader.readexactly(7)
|
||||
length = int.from_bytes(header[4:6], "big")
|
||||
body = await reader.readexactly(length - 1)
|
||||
return header, body
|
||||
|
||||
|
||||
class TestProtocolRoundtrip:
|
||||
async def test_read_constants(self, app):
|
||||
await _start(app)
|
||||
reader, writer = await asyncio.open_connection("127.0.0.1", 15020)
|
||||
# FC3 read 9 words at 0x2000
|
||||
pdu = bytes([FC_READ_HOLDING_REGISTERS]) + (0x2000).to_bytes(2, "big") + (9).to_bytes(2, "big")
|
||||
_, body = await _roundtrip(reader, writer, 7, pdu)
|
||||
assert body[0] == FC_READ_HOLDING_REGISTERS
|
||||
assert body[1] == 18
|
||||
words = [int.from_bytes(body[2 + 2 * i:4 + 2 * i], "big") for i in range(9)]
|
||||
assert words == [0x0000, 0xFFFF, 0x1234, 0xAAAA, 0x5555,
|
||||
0x7FFF, 0x8000, 0x3FFF, 0x4000]
|
||||
writer.close()
|
||||
await app.server.stop()
|
||||
|
||||
async def test_write_and_readback(self, app):
|
||||
await _start(app)
|
||||
reader, writer = await asyncio.open_connection("127.0.0.1", 15020)
|
||||
# FC6 write single register 0x1234 to address 0
|
||||
pdu = bytes([FC_WRITE_SINGLE_REGISTER]) + (0).to_bytes(2, "big") + (0x1234).to_bytes(2, "big")
|
||||
_, body = await _roundtrip(reader, writer, 1, pdu)
|
||||
assert body == bytes([FC_WRITE_SINGLE_REGISTER]) + (0).to_bytes(2, "big") + (0x1234).to_bytes(2, "big")
|
||||
# FC4 read back at 0x0200 mirror
|
||||
pdu = bytes([FC_READ_INPUT_REGISTERS]) + (0x0200).to_bytes(2, "big") + (1).to_bytes(2, "big")
|
||||
_, body = await _roundtrip(reader, writer, 2, pdu)
|
||||
# response PDU: [FC, byte_count, hi, lo]
|
||||
assert int.from_bytes(body[2:4], "big") == 0x1234
|
||||
writer.close()
|
||||
await app.server.stop()
|
||||
|
||||
async def test_coil_write_and_read(self, app):
|
||||
await _start(app)
|
||||
reader, writer = await asyncio.open_connection("127.0.0.1", 15020)
|
||||
# FC5 write coil address 3 ON
|
||||
pdu = bytes([FC_WRITE_SINGLE_COIL]) + (3).to_bytes(2, "big") + b"\xFF\x00"
|
||||
_, body = await _roundtrip(reader, writer, 3, pdu)
|
||||
assert body[0] == FC_WRITE_SINGLE_COIL
|
||||
# FC1 read coils at 0x0200 (output mirror)
|
||||
pdu = bytes([FC_READ_COILS]) + (0x0200).to_bytes(2, "big") + (8).to_bytes(2, "big")
|
||||
_, body = await _roundtrip(reader, writer, 4, pdu)
|
||||
assert body[2] & (1 << 3)
|
||||
writer.close()
|
||||
await app.server.stop()
|
||||
|
||||
async def test_illegal_address_exception(self, app):
|
||||
await _start(app)
|
||||
reader, writer = await asyncio.open_connection("127.0.0.1", 15020)
|
||||
pdu = bytes([FC_READ_HOLDING_REGISTERS]) + (0x0100).to_bytes(2, "big") + (1).to_bytes(2, "big")
|
||||
_, body = await _roundtrip(reader, writer, 5, pdu)
|
||||
assert body[0] == FC_READ_HOLDING_REGISTERS | 0x80
|
||||
assert body[1] == 0x02
|
||||
writer.close()
|
||||
await app.server.stop()
|
||||
|
||||
async def test_unsupported_function(self, app):
|
||||
await _start(app)
|
||||
reader, writer = await asyncio.open_connection("127.0.0.1", 15020)
|
||||
pdu = bytes([0x07]) + b"\x00\x00"
|
||||
_, body = await _roundtrip(reader, writer, 6, pdu)
|
||||
assert body[0] == 0x87
|
||||
assert body[1] == 0x01
|
||||
assert app.stats.bad_function == 1
|
||||
writer.close()
|
||||
await app.server.stop()
|
||||
|
||||
async def test_statistics_after_errors(self, app):
|
||||
await _start(app)
|
||||
reader, writer = await asyncio.open_connection("127.0.0.1", 15020)
|
||||
# two illegal-address reads
|
||||
for txn in (10, 11):
|
||||
pdu = bytes([FC_READ_HOLDING_REGISTERS]) + (0x0300).to_bytes(2, "big") + (1).to_bytes(2, "big")
|
||||
await _roundtrip(reader, writer, txn, pdu)
|
||||
assert app.stats.bad_address == 2
|
||||
# read statistics block via FC3 at 0x1029
|
||||
pdu = bytes([FC_READ_HOLDING_REGISTERS]) + (0x1029).to_bytes(2, "big") + (9).to_bytes(2, "big")
|
||||
_, body = await _roundtrip(reader, writer, 12, pdu)
|
||||
words = [int.from_bytes(body[2 + 2 * i:4 + 2 * i], "big") for i in range(9)]
|
||||
assert words[4] == 2 # BadAddress
|
||||
assert words[8] == 3 # ModTcpMessageCounter
|
||||
writer.close()
|
||||
await app.server.stop()
|
||||
|
||||
async def test_watchdog_expiry_blocks_process_data(self, app):
|
||||
await _start(app)
|
||||
reader, writer = await asyncio.open_connection("127.0.0.1", 15020)
|
||||
# configure watchdog: 1 unit = 100 ms, monitor FC3 (bit 2)
|
||||
for txn, (addr, val) in enumerate([(0x1000, 1), (0x1001, 0x0004)], start=20):
|
||||
pdu = bytes([FC_WRITE_SINGLE_REGISTER]) + addr.to_bytes(2, "big") + val.to_bytes(2, "big")
|
||||
await _roundtrip(reader, writer, txn, pdu)
|
||||
# trigger with alternating values
|
||||
pdu = bytes([FC_WRITE_SINGLE_REGISTER]) + (0x1003).to_bytes(2, "big") + (1).to_bytes(2, "big")
|
||||
await _roundtrip(reader, writer, 30, pdu)
|
||||
# let it expire: 100ms without masked FC3
|
||||
await asyncio.sleep(0.2)
|
||||
# process data read must now answer 0x04
|
||||
pdu = bytes([FC_READ_HOLDING_REGISTERS]) + (0).to_bytes(2, "big") + (1).to_bytes(2, "big")
|
||||
_, body = await _roundtrip(reader, writer, 31, pdu)
|
||||
assert body[0] == FC_READ_HOLDING_REGISTERS | 0x80
|
||||
assert body[1] == 0x04
|
||||
# watchdog registers remain accessible: status reads expired (2)
|
||||
pdu = bytes([FC_READ_HOLDING_REGISTERS]) + (0x1006).to_bytes(2, "big") + (1).to_bytes(2, "big")
|
||||
_, body = await _roundtrip(reader, writer, 32, pdu)
|
||||
# response PDU: [FC, byte_count, hi, lo]
|
||||
assert int.from_bytes(body[2:4], "big") == 2
|
||||
writer.close()
|
||||
await app.server.stop()
|
||||
Reference in New Issue
Block a user