@@ -74,8 +74,13 @@ def __init__(self) -> None:
7474 self .release_read = threading .Event ()
7575 self .entered_read = threading .Event ()
7676 self .read_calls : list [tuple [int , int ]] = []
77+ self .try_cached_read_calls : list [tuple [int , int ]] = []
7778 self .close_calls = 0
7879 self ._payload = b"X"
80+ # When set, ``try_cached_read`` returns a slice of this buffer
81+ # so the StreamHandle fast path can be exercised. Default ``None``
82+ # means cache miss — every call falls back to ``read``.
83+ self .cached_payload : bytes | None = None
7984
8085 def set_payload (self , body : bytes ) -> None :
8186 self ._payload = body
@@ -86,6 +91,12 @@ def read(self, off: int, size: int) -> bytes:
8691 self .release_read .wait ()
8792 return self ._payload
8893
94+ def try_cached_read (self , off : int , size : int ) -> bytes | None :
95+ self .try_cached_read_calls .append ((off , size ))
96+ if self .cached_payload is None :
97+ return None
98+ return self .cached_payload [off : off + size ]
99+
89100 def close (self ) -> None :
90101 self .close_calls += 1
91102
@@ -140,6 +151,63 @@ async def test_read_after_close_raises_eio(self):
140151 assert excinfo .value .errno == errno .EIO
141152
142153
154+ class TestStreamHandleCachedFastPath :
155+ async def test_cache_hit_returns_bytes_without_thread_dispatch (self ):
156+ encoder = _FakeEncoder ()
157+ encoder .cached_payload = b"hello"
158+ # release_read deliberately not set: if the fast path doesn't
159+ # short-circuit, the slow-path worker thread will block forever
160+ # and the test will hang.
161+ handle = encoder_host .StreamHandle (encoder )
162+ try :
163+ got = await handle .read (0 , 5 )
164+ assert got == b"hello"
165+ assert encoder .try_cached_read_calls == [(0 , 5 )]
166+ assert encoder .read_calls == []
167+ assert not encoder .entered_read .is_set ()
168+ finally :
169+ encoder .release_read .set ()
170+ await handle .aclose ()
171+
172+ async def test_cache_miss_falls_back_to_slow_path (self ):
173+ encoder = _FakeEncoder ()
174+ encoder .set_payload (b"world" )
175+ encoder .release_read .set ()
176+ handle = encoder_host .StreamHandle (encoder )
177+ try :
178+ got = await handle .read (0 , 5 )
179+ assert got == b"world"
180+ assert encoder .try_cached_read_calls == [(0 , 5 )]
181+ assert encoder .read_calls == [(0 , 5 )]
182+ finally :
183+ await handle .aclose ()
184+
185+ async def test_cache_hit_does_not_enter_encoder_read (self ):
186+ encoder = _FakeEncoder ()
187+ encoder .cached_payload = b"ABCDEFGH"
188+ handle = encoder_host .StreamHandle (encoder )
189+ try :
190+ async with trio .open_nursery () as nursery :
191+ for off in range (4 ):
192+ nursery .start_soon (handle .read , off , 2 )
193+ assert len (encoder .try_cached_read_calls ) == 4
194+ assert encoder .read_calls == []
195+ finally :
196+ encoder .release_read .set ()
197+ await handle .aclose ()
198+
199+ async def test_cache_hit_after_close_raises_eio_without_calling_encoder (self ):
200+ encoder = _FakeEncoder ()
201+ encoder .cached_payload = b"never seen"
202+ encoder .release_read .set ()
203+ handle = encoder_host .StreamHandle (encoder )
204+ await handle .aclose ()
205+ with pytest .raises (OSError , match = "stream handle is closed" ) as excinfo :
206+ await handle .read (0 , 1 )
207+ assert excinfo .value .errno == errno .EIO
208+ assert encoder .try_cached_read_calls == []
209+
210+
143211class TestStreamHandleSerialisation :
144212 async def test_concurrent_reads_serialise_on_one_handle (self ):
145213 """Two concurrent ``handle.read`` calls must not enter the
0 commit comments