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