Skip to content

test: make TrickleReader honour the destination it is given - #45

Closed
lalinsky wants to merge 1 commit into
mainfrom
fix/trickle-reader-vectored
Closed

test: make TrickleReader honour the destination it is given#45
lalinsky wants to merge 1 commit into
mainfrom
fix/trickle-reader-vectored

Conversation

@lalinsky

@lalinsky lalinsky commented Sep 5, 2026

Copy link
Copy Markdown
Owner

Fixes #44.

TrickleReader always wrote its byte into the reader's own buffer and returned 1. That suits only one of the two drivers in std.Io.Reader:

  • fillUnbuffered passes an empty data[0], expects r.buffer to grow, and ignores the return value
  • readSliceShort passes the caller's remaining destination and advances by the returned count

Under the second, the old implementation wrote the byte somewhere the caller never looks while claiming it had been delivered — and once r.buffer filled it returned 0 with no progress, so the loop spun forever.

fn readVec(r: *std.Io.Reader, data: [][]u8) std.Io.Reader.Error!usize {
    _ = data;                                   // <- the destination, discarded
    ...
    if (r.end >= r.buffer.len) return 0;        // <- no progress, caller loops
    r.buffer[r.end] = self.data[self.pos];
    ...
    return 1;                                   // <- but nothing reached data[0]
}

Now it branches on whether a destination was offered, and returns 0 for buffer writes as the vtable contract describes ("Implementations may ignore data, writing directly to Reader.buffer ... and returning 0").

Why it went unnoticed

Every existing test keeps values inside the buffer, where readSliceShort's opening memcpy satisfies the whole read and the loop never runs. The remaining tests assert error.ReaderBufferTooSmall, which the library raises before reading. So the helper was only ever exercised below the threshold its own doc comment advertises — that the buffer can be smaller than the value being decoded.

The hang is also the worst failure mode: no output, no assertion, just a timeout, with suspicion landing on the library rather than the harness.

The test it was blocking

A 200-byte string decoded through buffers of 16, 33 and 64 bytes. String values are copied out rather than borrowed, so unlike map keys they have no size limit relative to the reader's buffer — a real case with no coverage until now.

Verified both ways: with the old readVec the new test does not terminate (killed at 45s); with the fix the suite runs in the usual few hundred milliseconds.

Still not implemented

stream and discard return error.EndOfStream unconditionally. Nothing needs them yet, but skipAny uses discardAll, so a future skip test against this reader will fail — loudly and immediately, not by hanging.

zig build test: 184/184 pass, up from 183.

Summary by CodeRabbit

  • Bug Fixes
    • Improved decoding of string values larger than the reader’s buffer.
    • Fixed handling of reads into caller-provided destination slices.
  • Tests
    • Added coverage for decoding oversized strings through the destination-slice path.

TrickleReader always wrote its byte into the reader's own buffer and returned
1, which suits only one of the two drivers in std.Io.Reader.

fillUnbuffered passes an empty data[0], expects the reader's buffer to grow,
and ignores the return value. readSliceShort passes the caller's remaining
destination and advances by the returned count. Under the latter the old
implementation wrote the byte somewhere the caller never looks while claiming
it had been delivered, and once the reader's buffer filled it returned 0 with
no progress, so the loop spun forever.

Nothing hit it because every existing test kept values inside the buffer, where
readSliceShort's opening memcpy satisfies the whole read and the loop never
runs. That is below the threshold the type's own doc comment advertises, which
says the buffer can be smaller than the value being decoded.

Branch on whether a destination was offered, and return 0 for buffer writes as
the vtable contract describes.

Adds the test this was blocking: a 200 byte string decoded through buffers of
16, 33 and 64 bytes. String values are copied out rather than borrowed, so
unlike map keys they have no size limit relative to the reader's buffer. With
the old readVec that test does not terminate.
@coderabbitai

coderabbitai Bot commented Sep 5, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Team

Run ID: 10f27823-c4ad-467b-8828-be9be7200f6b

📥 Commits

Reviewing files that changed from the base of the PR and between 52b624d and dc07772.

📒 Files selected for processing (1)
  • src/msgpack.zig

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The change updates TrickleReader.readVec to honor caller-provided destination slices and adds coverage for decoding a 200-byte string with smaller reader buffers.

Changes

TrickleReader read-path correction

Layer / File(s) Summary
Destination reads and long-value coverage
src/msgpack.zig
readVec writes one byte to a supplied destination slice and retains internal-buffer filling for unbuffered reads. The test decodes a 200-byte string with buffer sizes 16, 33, and 64.

Estimated code review effort: 3 (Moderate) | ~15–30 minutes

Merge Risk: ⚪ Minimal · up to dc077

TrickleReader now correctly delivers oversized decoded strings through caller-provided buffers, avoiding the prior zero-progress hang. The added coverage validates this behavior across multiple small buffer sizes, with no remaining merge-readiness risk identified.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: making TrickleReader honor the destination supplied to it.
Linked Issues check ✅ Passed The changes satisfy issue #44. readVec writes to the caller-provided destination when available, preserves buffer filling when no destination is provided, and adds coverage for string values larger …
Out of Scope Changes check ✅ Passed The changes are limited to the TrickleReader fix and its regression test. No unrelated code changes are described.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 0…
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/trickle-reader-vectored

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@lalinsky

lalinsky commented Sep 5, 2026

Copy link
Copy Markdown
Owner Author

Superseded by #46, which implements stream and deletes the readVec and discard overrides instead. stream is the only vtable field without a default, so deriving the rest from it is both less code and correct in cases this PR would still have missed — discard (needed by skipAny) and stream itself.

@lalinsky lalinsky closed this Sep 5, 2026
@lalinsky
lalinsky deleted the fix/trickle-reader-vectored branch September 5, 2026 06:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

TrickleReader hangs on values larger than its buffer, which is what it claims to model

1 participant