Files
wago-modbustcp-simulator-py/tests/test_registers.py
T
2026-10-03 09:46:11 +02:00

223 lines
7.5 KiB
Python

"""Register bank, statistics, and watchdog tests."""
from __future__ import annotations
import pytest
from wago_sim.registers import ConfigRegisterBank
from wago_sim.stats import ModbusStatistics
from wago_sim.watchdog import (
ModbusWatchdog,
RegisterWriteRefused,
WD_CLOSE_ON_TIMEOUT,
WD_MASK_1_16,
WD_SIMPLE_STOP,
WD_STATUS,
WD_STATUS_ACTIVE,
WD_STATUS_EXPIRED,
WD_STATUS_INACTIVE,
WD_TIME,
WD_TRIGGER,
WD_TYPE,
)
class TestConstants:
"""Acceptance criterion 1: constant registers 0x2000-0x2008."""
@pytest.mark.parametrize("address,expected", [
(0x2000, 0x0000),
(0x2001, 0xFFFF),
(0x2002, 0x1234),
(0x2003, 0xAAAA),
(0x2004, 0x5555),
(0x2005, 0x7FFF),
(0x2006, 0x8000),
(0x2007, 0x3FFF),
(0x2008, 0x4000),
])
def test_constant_block(self, app, address, expected):
assert app.registers.read_config(address, 1) == [expected]
def test_constant_block_bulk(self, app):
words = app.registers.read_config(0x2000, 9)
assert words == [0x0000, 0xFFFF, 0x1234, 0xAAAA, 0x5555,
0x7FFF, 0x8000, 0x3FFF, 0x4000]
class TestFirmwareInfo:
def test_series_code_is_750(self, app):
assert app.registers.read_config(0x2011, 1) == [0x02EE]
def test_description_words(self, app):
words = app.registers.read_config(0x2020, 16)
text = "".join(chr((w >> 8) & 0xFF) + chr(w & 0xFF) for w in words)
assert text.startswith("WAGO 750-362 Simulator")
def test_image_sizes(self, app):
# pump node: 2 AI words + 2 AO words, 24 DI, 24 DO
assert app.registers.read_config(0x1022, 1) == [2 * 16] # analog out bits
assert app.registers.read_config(0x1023, 1) == [2 * 16] # analog in bits
assert app.registers.read_config(0x1024, 1) == [24] # digital out bits
assert app.registers.read_config(0x1025, 1) == [24] # digital in bits
class TestStatistics:
def test_initial_zero(self, app):
words = app.registers.read_config(0x1029, 9)
assert words == [0] * 9
def test_counter_readback(self, app):
app.stats.bad_address = 3
app.stats.mod_tcp_message_counter = 42
words = app.registers.read_config(0x1029, 9)
assert words[4] == 3
assert words[8] == 42
def test_reset_via_magic_write(self, app):
app.stats.bad_address = 3
app.stats.mod_tcp_message_counter = 42
app.registers.write_config(0x1029, [0xAA55])
words = app.registers.read_config(0x1029, 9)
assert words == [0] * 9
def test_reset_key_55aa(self):
stats = ModbusStatistics()
stats.bad_length = 1
assert stats.handle_reset_write(0x55AA) is True
assert stats.bad_length == 0
assert stats.handle_reset_write(0x1234) is False
class TestWatchdog:
def _make(self):
expired = []
wd = ModbusWatchdog(on_expire=lambda: expired.append(1))
return wd, expired
def test_defaults(self):
wd, _ = self._make()
assert wd.read(WD_TIME) == 0x0064
assert wd.read(WD_MASK_1_16) == 0xFFFF
assert wd.read(WD_STATUS) == WD_STATUS_INACTIVE
def test_cannot_change_time_while_active(self):
wd, _ = self._make()
wd.write(WD_TRIGGER, 5) # start via trigger
with pytest.raises(RegisterWriteRefused):
wd.write(WD_TIME, 20)
def test_trigger_starts_and_expires(self):
wd, expired = self._make()
wd.write(WD_TIME, 5) # 500 ms
wd.write(WD_TRIGGER, 1)
assert wd.read(WD_STATUS) == WD_STATUS_ACTIVE
for _ in range(5):
wd.tick()
assert wd.read(WD_STATUS) == WD_STATUS_EXPIRED
assert expired == [1]
assert wd.process_data_blocked is True # Standard mode
def test_trigger_alternating_kicks(self):
wd, expired = self._make()
wd.write(WD_TIME, 5)
wd.write(WD_TRIGGER, 1)
for i in range(10):
wd.write(WD_TRIGGER, i % 2) # alternating 0/1 values kick
for _ in range(3):
wd.tick()
assert wd.read(WD_STATUS) == WD_STATUS_ACTIVE
assert expired == []
def test_trigger_same_value_does_not_kick(self):
wd, expired = self._make()
wd.write(WD_TIME, 2)
wd.write(WD_TRIGGER, 1)
for _ in range(4):
wd.tick()
assert wd.read(WD_STATUS) == WD_STATUS_EXPIRED
def test_function_code_mask_kick(self):
wd, expired = self._make()
wd.write(WD_TIME, 5)
wd.write(WD_MASK_1_16, 1 << 4) # only FC5 monitored
wd.write(WD_TRIGGER, 1)
wd.on_function_code(3) # not masked -> no kick
for _ in range(4):
wd.tick()
assert wd.read(WD_STATUS) == WD_STATUS_ACTIVE
wd.on_function_code(5) # masked -> kick
for _ in range(4):
wd.tick()
assert wd.read(WD_STATUS) == WD_STATUS_ACTIVE
wd.on_function_code(5)
for _ in range(5):
wd.tick()
assert wd.read(WD_STATUS) == WD_STATUS_EXPIRED
assert expired == [1]
def test_simple_stop(self):
wd, expired = self._make()
wd.write(WD_TIME, 2)
wd.write(WD_TRIGGER, 1)
wd.write(WD_SIMPLE_STOP, 0xAA55)
assert wd.read(WD_STATUS) == WD_STATUS_INACTIVE
for _ in range(5):
wd.tick()
assert expired == []
def test_restart_after_expiry(self):
wd, _ = self._make()
wd.write(WD_TIME, 2)
wd.write(WD_TRIGGER, 1)
wd.tick(); wd.tick()
assert wd.read(WD_STATUS) == WD_STATUS_EXPIRED
wd.write(0x1007, 1) # restart
assert wd.read(WD_STATUS) == WD_STATUS_ACTIVE
assert wd.process_data_blocked is False
def test_alternative_mode_restart_on_read(self):
wd, _ = self._make()
wd.write(WD_TYPE, 1) # Alternative
wd.write(WD_TIME, 2)
wd.write(WD_TRIGGER, 1)
wd.tick(); wd.tick()
# first read returns "expired" and restarts the watchdog
assert wd.read(WD_STATUS) == WD_STATUS_EXPIRED
# subsequent read shows "active" again
assert wd.read(WD_STATUS) == WD_STATUS_ACTIVE
assert wd.process_data_blocked is False
def test_close_connections_flag(self):
wd, _ = self._make()
assert wd.close_connections_on_timeout is False
wd.write(WD_CLOSE_ON_TIMEOUT, 1)
assert wd.close_connections_on_timeout is True
class TestMiscRegisters:
def test_mac_id(self, app):
words = app.registers.read_config(0x1031, 3)
# register exposes the first 3 MAC bytes, one per word (high byte)
assert words == [0x00 << 8, 0x30 << 8, 0xDE << 8]
def test_response_delay_writable(self, app):
app.registers.write_config(0x1037, [25])
assert app.registers.response_delay_ms == 25
def test_idle_timeout_writable(self, app):
app.registers.write_config(0x1030, [10])
assert app.registers.idle_timeout_ms == 1000
def test_readonly_register_rejects_write(self, app):
from wago_sim.watchdog import RegisterWriteRefused
with pytest.raises(RegisterWriteRefused):
app.registers.write_config(0x2002, [0x1111])
def test_led_error_injection(self, app):
app.registers.set_led_error(0x12, 0x34)
assert app.registers.read_config(0x1020, 2) == [0x12, 0x34]
def test_module_diagnosis_injection(self, app):
app.registers.set_module_diagnosis(module=2, channel=1, diagnosis=0b100)
assert app.registers.read_config(0x1050, 3) == [2, 1, 0b100]