🎉 Initial commit
This commit is contained in:
@@ -0,0 +1,223 @@
|
||||
"""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]
|
||||
Reference in New Issue
Block a user