Repository navigation
Conversation
PgServerMock can now play a walsender: CopyBothResponse, CopyData, XLogData and keepalive messages, CopyDone, CopyOutResponse and NoticeResponse writers, NULL DataRow columns, a configurable server_version, the parsed startup parameters (e.g. "replication") and Expect helpers for CopyDone, Terminate and standby status updates. This also fixes length bugs that were hidden because the client parses these messages by content (CopyInResponse, ParameterDescription and ErrorResponse sent wrong lengths), and ExpectMessage skipped synchronously without ensuring the message was fully received. ReplicationMock opens replication connections against the mock with the feedback timers disabled, and ReplicationMockTests smoke-tests startup, IDENTIFY_SYSTEM, physical streaming, keepalive replies and cancellation, with timeouts on both the client and the mock side. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
Non-ASCII error and notice fields use inconsistent encodings for lengths and payloads, which can stall client reads.
1 open issue
What changed in this PR
Extends the test server mock to emulate PostgreSQL replication, allowing replication behavior to be tested without a live server.
Changes:
- Adds copy-both, replication message writers, and standby status expectations.
- Exposes startup parameters and configurable server versions, supports NULL columns, and corrects message lengths and asynchronous skipping.
- Adds replication connection helpers and smoke tests for startup, streaming, keepalive replies, and cancellation.
| File | Description |
|---|---|
| test/Npgsql.Tests/Support/ReplicationMock.cs | Adds replication connection and response helpers. |
| test/Npgsql.Tests/Support/PgServerMock.cs | Adds replication protocol support and fixes message framing. |
| test/Npgsql.Tests/Support/PgPostmasterMock.cs | Makes the advertised server version configurable. |
| test/Npgsql.Tests/Replication/ReplicationMockTests.cs | Tests replication startup, streaming, feedback, and cancellation. |
Review effort: Balanced
| 1 + Encoding.GetByteCount(code) + 1 + | ||
| 1 + Encoding.GetByteCount(severity) + 1 + | ||
| 1 + Encoding.GetByteCount(message) + 1 + |
roji
left a comment
There was a problem hiding this comment.
Recommendation
The core direction is justified. I found no correctness blocker in the exercised paths. I would keep the replication mock and smoke tests, trim unused future-facing helpers, and preserve the important transport fixes with focused regression coverage.
Problem and expected approach
There is a real testability gap: the existing physical replication fixture is explicitly disabled for flakiness, and its streaming tests depend on cluster-wide WAL activity. The shared replication fixture also requires a specially configured live server. A mock cannot replace those integration tests, but it can make client protocol sequencing deterministic.
Independently of this implementation, I would extend the existing wire mock with CopyBothResponse, WAL and keepalive writers, feedback parsing, and a small connection helper that disables automatic feedback. A few hundred lines of C# test-only code is reasonable; no production API or user-documentation change is needed. The essential scenarios are startup/IDENTIFY_SYSTEM, a streamed payload, requested feedback, and cancellation followed by connection reuse.
Alignment
The PR follows that design without adding a separate mocking framework. Its physical smoke test drives the real replication API while scripting the peer. For example, after starting a client MoveNextAsync:
await server.ExpectSimpleQuery("START_REPLICATION PHYSICAL 0/1000000");
await server.WriteCopyBothResponse()
.WriteXLogData(new(0x1000000), new(0x1000003), "abc"u8)
.FlushAsync();The NULL DataRow support is relevant to physical IDENTIFY_SYSTEM. The adjacent length corrections are also legitimate: ParameterDescription previously counted the message-type byte, CopyInResponse declared 5 bytes while emitting 9, and ErrorResponse omitted three string terminators from its declared length. The asynchronous skip fixes consumption of fragmented frontend bodies rather than merely changing style.
Gaps and scope
The new tests do not exercise the changed ExpectMessage skip or the large-payload flushing writer; the inline comments suggest narrow regression cases. Also, CopyOutResponse, raw CopyData/CopyDone writers, and server-version configurability have no consuming tests in this PR. I would defer that unused surface until a concrete scenario needs it rather than treating this as a general protocol-mock expansion. These are coverage/scope recommendations, not demonstrated runtime defects. The smoke tests establish the foundation; they do not yet cover timeline switches, server shutdown, or logical decoding streams.
Manual validation
I built a separate console program against the exact PR mock sources and drove real logical and physical replication connections, independently of NUnit. It verified a 24,593-byte WAL payload, high-bit LSNs, an exact server timestamp, notice handling, all three feedback LSNs, cancellation routing, connection reuse, a NULL database name, and Terminate. A separate raw TCP peer checked the corrected frame lengths, COPY/NULL encodings, a WAL header near the buffer boundary, and a 20 KB frontend body delivered separately from its header, including alignment of subsequent messages. All passed. The two new smoke tests and eight selected existing mock tests also passed. Validation used an extracted source copy and the installed .NET 11 preview SDK because the pinned 10.0.402 SDK was unavailable; no reviewed source was changed.
Posted by copilot
| $"Expected message of type '{(char)expectedCode}' but got '{(char)actualCode}'"); | ||
| var len = _readBuffer.ReadInt32(); | ||
| _readBuffer.Skip(len - 4); | ||
| await _readBuffer.Skip(async: true, len - 4); |
There was a problem hiding this comment.
Could we add a focused regression case that sends a frontend header first, then delivers a body larger than the read buffer, and verifies the next message stays aligned? This fixes a real transport issue, but the new replication tests call ExpectSimpleQuery/ExpectStandbyStatusUpdate rather than this path. I manually exercised a fragmented 20 KB body and the new implementation worked; keeping that case in the suite would protect the fix.
Posted by copilot
| ReadOnlyMemory<byte> data, | ||
| DateTime? serverClock = null) | ||
| { | ||
| if (_writeBuffer.WriteSpaceLeft < XLogDataHeaderLength) |
There was a problem hiding this comment.
This flushing writer has no caller in the added tests: the only WAL payload is three bytes and uses WriteXLogData. If we keep this helper now, could we add one case with a payload larger than the 8192-byte buffer and less than XLogDataHeaderLength space remaining before the header? I manually checked both boundaries successfully, but neither branch currently has regression coverage. Otherwise I would defer this helper until its first consuming test.
Posted by copilot
roji
left a comment
There was a problem hiding this comment.
Please see agentic review below, am otherwise approving as this is test infra only.

PgServerMock can now play a walsender: CopyBothResponse, CopyData, XLogData and keepalive messages, CopyDone, CopyOutResponse and NoticeResponse writers, NULL DataRow columns, a configurable server_version, the parsed startup parameters (e.g. "replication") and Expect helpers for CopyDone, Terminate and standby status updates.
This also fixes length bugs that were hidden because the client parses these messages by content (CopyInResponse, ParameterDescription and ErrorResponse sent wrong lengths), and ExpectMessage skipped synchronously without ensuring the message was fully received.
ReplicationMock opens replication connections against the mock with the feedback timers disabled, and ReplicationMockTests smoke-tests startup, IDENTIFY_SYSTEM, physical streaming, keepalive replies and cancellation, with timeouts on both the client and the mock side.