watlowlib.testing¶
Public testing surface — FakeTransport, FakeSlave, fixture
records (StdBusRound, ModbusRound, Fixture), load_fixture, and
controller_from_fixture. See Testing.
Public surface¶
watlowlib.testing ¶
Public testing surface for downstream packages.
The test seam is a contract: anyone consuming :mod:watlowlib can
import watlowlib.testing and write facade-level tests against
captured PM3 round-trips without touching a serial port.
What's here:
- :class:
FakeTransport— re-export of the in-process scripted transport (fixture-replay grade: ordered queue + unmatched-write capture). - :class:
FakeSlave— stub :class:anymodbus.Slavefor the Modbus facade path (the equivalent shape for variants that emit a :class:ModbusOprather than wire bytes). - :class:
StdBusRound/ :class:ModbusRound/ :class:Fixture— typed records for one captured round-trip and a captured scenario. - :func:
load_fixture— JSONL file → :class:Fixture. - :func:
controller_from_fixture— JSONL file → opened :class:Controllerready for facade-level assertions. - :func:
parse_arrow_fixture— plaintext arrow file →dict[bytes, bytes]script map for :class:FakeTransport. The arrow format matches :mod:alicatlib.testingand :mod:sartoriuslib.testingfor cross-package consistency. - :func:
FakeTransportFromArrowFixture— plaintext arrow file → built :class:FakeTransport.
Two fixture formats are supported:
Plaintext arrow (Std Bus only — recommended for code review)::
# scenario: read_pv
> 55 FF 05 10 00 00 06 E8 01 03 01 04 01 01 E3 99
< 55 FF 06 00 10 00 0B 88 02 03 01 04 01 01 08 45 1E 3C D4 A7 28
Bytes are space-separated hex. # introduces comments; blank lines
are ignored. Each > line names a request; one or more following
< lines name the reply (concatenated into one scripted reply).
JSONL (rich — required for Modbus, optional for Std Bus). One JSON object per line. The first line may be a header recording the protocol and serial framing; everything else is a round.
Std Bus round::
{
"protocol": "stdbus",
"label": "read_pv",
"request_hex": "55FF051000...",
"response_hex": "55FF0600...",
}
Modbus round::
{
"protocol": "modbus_rtu",
"label": "read_pv",
"method": "read_holding_registers",
"address": 360,
"count": 2,
"response_words": [17299, 29054],
}
{
"protocol": "modbus_rtu",
"label": "set_setpoint",
"method": "write_registers",
"address": 2160,
"values": [17348, 0],
}
Optional header ("kind": "header") sets address, baudrate, and
parity for the whole capture::
{
"kind": "header",
"protocol": "stdbus",
"address": 1,
"baudrate": 38400,
"parity": "none",
}
FakeSlave ¶
Scripted :class:anymodbus.Slave stand-in for tests.
Mirrors the surface :class:watlowlib.protocol.modbus.client.ModbusProtocolClient
actually calls (read_holding_registers,
read_input_registers, write_register, write_registers)
and records every call. Tests assert on :attr:reads and
:attr:writes to verify the :class:ModbusOp lowered correctly.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
script
|
Mapping[tuple[str, int], ScriptedSlaveEntry] | None
|
|
None
|
Source code in src/watlowlib/transport/fake.py
add_script ¶
Register or overwrite a scripted reply for (method, address).
FakeTransport ¶
Scripted :class:Transport for tests.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
script
|
Mapping[bytes, ScriptedReply] | None
|
Mapping of |
None
|
label
|
str
|
Identifier used in errors. |
'fake://test'
|
latency_s
|
float
|
Per-operation artificial delay, useful for simulating a slow device. |
0.0
|
Source code in src/watlowlib/transport/fake.py
unmatched_writes
property
¶
Writes that didn't match any scripted reply, in order.
A test can assert transport.unmatched_writes == () to catch
accidentally-unscripted traffic — the corresponding read would
have timed out, but a precise assertion fails faster and points
at the right call.
add_script ¶
extend_queue ¶
feed ¶
Push unsolicited bytes into the read buffer.
Useful for simulating a device that left chatter on the line which the protocol client has to drain on recovery.
Source code in src/watlowlib/transport/fake.py
force_read_timeout ¶
force_write_timeout ¶
Fixture
dataclass
¶
Fixture(
protocol,
address=1,
serial_settings=(
lambda: SerialSettings(port="fake://fixture")
)(),
stdbus_rounds=(),
modbus_rounds=(),
)
A captured scenario — one wire protocol, many rounds.
fake_slave ¶
Build a :class:FakeSlave scripted with the Modbus rounds.
Read rounds populate the (method, address) → response_words
script entry; write rounds need no entry — :class:FakeSlave
treats unscripted writes as silent successes and records them
on :attr:FakeSlave.writes so tests can assert on the lowered
register words.
Source code in src/watlowlib/testing.py
fake_transport ¶
Build a :class:FakeTransport scripted with the Std Bus rounds.
Each captured request maps to its captured response in the dict-based script, so a test that issues the same parameter read twice gets the same response both times — appropriate for facade smoke tests where ordering doesn't matter.
Source code in src/watlowlib/testing.py
ModbusRound
dataclass
¶
One captured Modbus call.
For reads response_words carries the register tuple the slave
returned; values is empty. For writes values carries the
register words the controller sent and response_words is empty
(a successful write returns no payload).
StdBusRound
dataclass
¶
One captured Std Bus request/response pair.
FakeTransportFromArrowFixture ¶
Load a plaintext-arrow fixture into a built :class:FakeTransport.
Convenience wrapper around :func:parse_arrow_fixture plus
:class:FakeTransport construction. The returned transport is
not opened — the caller awaits .open() as usual.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str | Path
|
Path to the |
required |
label
|
str | None
|
Optional override for :attr: |
None
|
Source code in src/watlowlib/testing.py
controller_from_fixture
async
¶
Build an unopened :class:Controller scripted by path.
Returned in unopened form so the caller drives lifecycle through
async with. Std Bus fixtures wire through :class:FakeTransport;
Modbus fixtures wire through :class:FakeSlave (the
:class:Transport shim only carries lifecycle, since the Modbus
protocol client talks to its slave provider directly).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
path
|
str | Path
|
JSONL fixture path. |
required |
profile
|
DeviceProfile
|
Device profile to decode against. Defaults to
:data: |
EZZONE_PROFILE
|
Source code in src/watlowlib/testing.py
load_fixture ¶
Parse path as a JSONL fixture and return a :class:Fixture.
The first line may be a header ({"kind": "header", ...}) that
sets address and the serial framing for the capture. Subsequent
lines are rounds; their protocol field must agree with the
header (or with the first round, if the header is omitted).
Raises:
| Type | Description |
|---|---|
ValueError
|
malformed JSONL, missing fields, or a row whose
|
Source code in src/watlowlib/testing.py
open_test_controller
async
¶
open_test_controller(
transport,
*,
protocol=ProtocolKind.STDBUS,
address=1,
serial_settings=None,
profile=EZZONE_PROFILE,
wire_temperature_unit=None,
)
Build an opened :class:Controller over an existing :class:Transport.
Test-surface companion to :func:watlowlib.open_device. Production
code always goes through :func:open_device (which opens a real
serial port and runs auto-detect when asked); tests drive the
facade through a :class:FakeTransport and use this helper to
skip the serial-port plumbing.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
transport
|
Transport
|
The transport to wire the controller to. Typically
a :class: |
required |
protocol
|
ProtocolKind
|
Wire protocol. |
STDBUS
|
address
|
int
|
Bus address. |
1
|
serial_settings
|
SerialSettings | None
|
Optional override; defaults to a
|
None
|
profile
|
DeviceProfile
|
Device profile to decode against. Defaults to
:data: |
EZZONE_PROFILE
|
wire_temperature_unit
|
Unit | None
|
Already-coerced :class: |
None
|
Returns:
| Type | Description |
|---|---|
Controller
|
An opened :class: |
Controller
|
closing it (typically via |
Source code in src/watlowlib/testing.py
parse_arrow_fixture ¶
Parse a plaintext-arrow fixture into a :class:FakeTransport script.
The fixture format is intentionally human-skimmable so captured
Std Bus traffic round-trips through code review (cross-package
convention shared with :mod:alicatlib.testing and
:mod:sartoriuslib.testing)::
# scenario: read_pv (PM3, parameter 4001)
> 55 FF 05 10 00 00 06 E8 01 03 01 04 01 01 E3 99
< 55 FF 06 00 10 00 0B 88 02 03 01 04 01 01 08 45 1E 3C D4 A7 28
Parsing rules:
- Lines starting with
#are comments; ignored. - Blank lines are ignored.
>introduces a request — bytes after the marker are decoded as space-separated hex. Whitespace within a payload is ignored so callers can group bytes for readability.<introduces one reply — same hex encoding. Multiple<lines after a single>concatenate into one scripted reply (useful when a logical reply was captured across multiple reads).- Duplicate
>entries are a fixture error rather than a silent overwrite.
Returns:
| Type | Description |
|---|---|
dict[bytes, bytes]
|
Mapping |
dict[bytes, bytes]
|
class: |
Raises:
| Type | Description |
|---|---|
ValueError
|
On malformed lines, a |
FileNotFoundError
|
Via the underlying :meth: |
Source code in src/watlowlib/testing.py
478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 | |