"""Plain-text request/response tracing tests (verbose mode).""" from __future__ import annotations from wago_sim import pdu as pdu_mod from wago_sim.mbap import MbapHeader from wago_sim.trace import ( describe_address, describe_bit_address, format_raw, format_request, format_response, ) def _header(txn: int = 1, unit: int = 1) -> MbapHeader: return MbapHeader(transaction_id=txn, protocol_id=0, length=6, unit_id=unit) class TestAddressAnnotations: def test_word_areas(self): assert "%IW0" in describe_address(0x0000) assert "%QW0" in describe_address(0x0200) # read-back mirror assert "%IW256" in describe_address(0x6000) assert "%QW256" in describe_address(0x7000) assert "config" in describe_address(0x1000).lower() def test_write_mapping_differs_from_read(self): """Table 120 (writes): 0x0000-0x00FF is the OUTPUT area %QW0-255; Table 119 (reads): the same range is the INPUT area %IW0-255.""" assert "%QW0" in describe_address(0x0000, write=True) assert "%IW0" in describe_address(0x0000, write=False) assert "%QW5" in describe_address(0x0005, write=True) assert "mirror" in describe_address(0x0200, write=True) assert "read-back" in describe_address(0x0200, write=False) assert "%QW256" in describe_address(0x6000, write=True) def test_illegal_word_areas(self): for addr in (0x0100, 0x0300, 0x3000, 0x62FC, 0x72FC): assert "illegal" in describe_address(addr) assert "illegal" in describe_address(addr, write=True) def test_word_ranges(self): text = describe_address(0x0000, 4) assert "0x0000..0x0003" in text def test_bit_areas(self): assert "channel" in describe_bit_address(0) assert "read-back" in describe_bit_address(0x0200) assert "area 2" in describe_bit_address(0x8000) assert "illegal" in describe_bit_address(0x0400) class TestRequestFormatting: def test_read_holding_registers(self): req = pdu_mod.ReadRequest(pdu_mod.FC_READ_HOLDING_REGISTERS, 0x2002, 1) text = format_request(_header(), req) assert "FC03" in text assert "Read Holding Registers" in text assert "addr=0x2002" in text assert "count=1" in text assert "txn=0x0001" in text assert "unit=1" in text def test_read_coils_bit_annotation(self): req = pdu_mod.ReadRequest(pdu_mod.FC_READ_COILS, 0x0200, 8) text = format_request(_header(), req) assert "FC01" in text assert "read-back" in text def test_write_single_coil(self): req = pdu_mod.WriteSingleCoilRequest(pdu_mod.FC_WRITE_SINGLE_COIL, 5, True) text = format_request(_header(), req) assert "FC05" in text assert "value=ON" in text def test_write_single_register(self): req = pdu_mod.WriteSingleRegisterRequest( pdu_mod.FC_WRITE_SINGLE_REGISTER, 0x0000, 0x1234 ) text = format_request(_header(), req) assert "FC06" in text assert "value=0x1234" in text # write mapping: 0x0000 is %QW0 (output), not %IW0 assert "%QW0" in text def test_write_multiple_registers(self): req = pdu_mod.WriteMultipleRegistersRequest( pdu_mod.FC_WRITE_MULTIPLE_REGISTERS, 0x0000, (0x1234, 0x5678) ) text = format_request(_header(), req) assert "FC10" in text assert "0x1234" in text and "0x5678" in text def test_write_multiple_coils_bitstring(self): req = pdu_mod.WriteMultipleCoilsRequest( pdu_mod.FC_WRITE_MULTIPLE_COILS, 0x0000, (True, False, True) ) text = format_request(_header(), req) assert "values=101" in text def test_mask_write(self): req = pdu_mod.MaskWriteRegisterRequest( pdu_mod.FC_MASK_WRITE_REGISTER, 0x0000, 0x00F2, 0x0025 ) text = format_request(_header(), req) assert "FC16" in text assert "and=0x00f2" in text def test_read_write_multiple(self): req = pdu_mod.ReadWriteMultipleRegistersRequest( pdu_mod.FC_READ_WRITE_MULTIPLE_REGISTERS, 0x2002, 1, 0x0000, (0xBEEF,) ) text = format_request(_header(), req) assert "FC17" in text assert "read addr=0x2002" in text def test_event_count(self): req = pdu_mod.GetCommEventCountRequest(pdu_mod.FC_GET_COMM_EVENT_COUNT) text = format_request(_header(), req) assert "FC0B" in text assert "Get Comm Event Count" in text class TestResponseFormatting: def test_read_registers(self): resp = pdu_mod.ReadResponse(pdu_mod.FC_READ_HOLDING_REGISTERS) resp.registers = [0x1234] text = format_response(_header(), resp) assert "RSP" in text assert "1 regs" in text assert "0x1234" in text def test_read_bits(self): resp = pdu_mod.ReadResponse(pdu_mod.FC_READ_COILS) resp.bits = [True, False, True] text = format_response(_header(), resp) assert "3 bits" in text assert "101" in text def test_exception(self): resp = pdu_mod.ExceptionResponse( pdu_mod.FC_READ_HOLDING_REGISTERS, pdu_mod.EXC_ILLEGAL_DATA_ADDRESS, ) text = format_response(_header(), resp) assert "EXC" in text assert "0x02" in text assert "Illegal Data Address" in text def test_write_echo(self): resp = pdu_mod.WriteSingleRegisterResponse(0x0000, 0x1234) text = format_response(_header(), resp) assert "echo" in text def test_event_count_response(self): resp = pdu_mod.GetCommEventCountResponse(0, 42) text = format_response(_header(), resp) assert "events=42" in text class TestRawFormatting: def test_raw_pdu(self): text = format_raw(_header(), b"\x03\x20\x02\x00\x01") assert "RAW" in text assert "03 20 02 00 01" in text class TestServerVerboseIntegration: """The server funnels every transaction through the trace formatter.""" def test_trace_lines_in_logs(self, app, caplog): import asyncio import logging app.server.host, app.server.port = "127.0.0.1", 15030 async def scenario(): await app.server.start() await asyncio.sleep(0.05) reader, writer = await asyncio.open_connection("127.0.0.1", 15030) pdu = bytes([pdu_mod.FC_READ_HOLDING_REGISTERS]) + (0x2002).to_bytes(2, "big") + (1).to_bytes(2, "big") from wago_sim.mbap import build_frame writer.write(build_frame(1, 1, pdu)) await writer.drain() header = await reader.readexactly(7) length = int.from_bytes(header[4:6], "big") body = await reader.readexactly(length - 1) writer.close() await app.server.stop() return body with caplog.at_level(logging.DEBUG, logger="wago_sim.server"): body = asyncio.run(scenario()) assert int.from_bytes(body[2:4], "big") == 0x1234 assert any("REQ" in rec.message for rec in caplog.records) assert any("RSP" in rec.message for rec in caplog.records) assert any("Read Holding Registers" in rec.message for rec in caplog.records)