@@ -54,43 +54,97 @@ def test_bytes_written():
5454
5555
5656class _RawWriteBlockOnce :
57- """Mock raw.write() that returns None on the first call , then writes normally."""
57+ """Mock raw.write() returning None once , then writing normally."""
5858
5959 def __init__ (self ):
6060 """Initialize _RawWriteBlockOnce."""
6161 self .call_count = 0
6262 self .written = bytearray ()
6363
6464 def __call__ (self , chunk ):
65- """Return None on first call to simulate a blocked socket write."""
65+ """Return None on first call to simulate a blocked write."""
6666 self .call_count += 1
6767 if self .call_count == 1 :
68- return (
69- None # simulates socket returning None on first blocked write
70- )
68+ return None
7169 self .written .extend (chunk )
7270 return len (chunk )
7371
72+ def fileno (self ):
73+ """Return a fake fd for select()."""
74+ return - 1
7475
75- def test_flush_when_raw_write_returns_none ():
76- """_flush_unlocked() must not treat None from raw.write() as a byte count.
7776
78- io.RawIOBase.write() returns None when a non-blocking socket cannot accept
79- data. del self._write_buf[:None] is equivalent to del self._write_buf[:]
80- which silently clears the entire buffer, truncating the response without
81- raising an exception.
77+ class _RawWriteBlockAlways :
78+ """Mock raw.write() that always returns None."""
79+
80+ def __init__ (self ):
81+ """Initialize _RawWriteBlockAlways."""
82+ self .call_count = 0
83+
84+ def __call__ (self , chunk ):
85+ """Return None to simulate a permanently blocked socket."""
86+ self .call_count += 1
87+
88+ def fileno (self ):
89+ """Return a fake fd for select()."""
90+ return - 1
91+
92+
93+ def test_flush_recovers_from_temporary_block (monkeypatch ):
94+ """_flush_unlocked() retries after select when raw.write() returns None.
95+
96+ A temporarily blocked socket should recover once select() reports
97+ the socket is writable again, delivering all buffered data.
8298 """
83- data = b'x' * (makefile .SOCK_WRITE_BLOCKSIZE * 2 ) # stress the write loop
99+ data = b'x' * (makefile .SOCK_WRITE_BLOCKSIZE * 2 )
84100
85101 sock = MockSocket ()
86102 wfile = makefile .MakeFile (sock , 'w' )
87103 wfile ._write_buf .extend (data )
88104
89105 mock = _RawWriteBlockOnce ()
90106 wfile .raw .write = mock
107+
108+ # select() reports writable immediately
109+ monkeypatch .setattr (
110+ 'cheroot.makefile.select.select' ,
111+ lambda _rlist , wlist , _xlist , _timeout : ([], wlist , []),
112+ )
91113 wfile ._flush_unlocked ()
92114
93115 assert bytes (mock .written ) == data , (
94- f'Expected { len (data )} bytes but only { len (mock .written )} reached raw.write(): '
95- 'buffer was silently discarded when raw.write() returned None'
116+ 'all buffered data should be written after select retry'
117+ )
118+
119+
120+ def test_flush_raises_on_sustained_block (monkeypatch ):
121+ """_flush_unlocked() raises BlockingIOError after select timeout.
122+
123+ If the socket stays blocked past SOCK_WRITE_TIMEOUT, the write
124+ buffer must be preserved and BlockingIOError raised.
125+ """
126+ import io
127+
128+ import pytest
129+
130+ data = b'x' * makefile .SOCK_WRITE_BLOCKSIZE
131+
132+ sock = MockSocket ()
133+ wfile = makefile .MakeFile (sock , 'w' )
134+ wfile ._write_buf .extend (data )
135+
136+ mock = _RawWriteBlockAlways ()
137+ wfile .raw .write = mock
138+
139+ # select() reports not writable (timeout)
140+ monkeypatch .setattr (
141+ 'cheroot.makefile.select.select' ,
142+ lambda _rlist , _wlist , _xlist , _timeout : ([], [], []),
143+ )
144+
145+ with pytest .raises (io .BlockingIOError ):
146+ wfile ._flush_unlocked ()
147+
148+ assert len (wfile ._write_buf ) == len (data ), (
149+ 'write buffer must be preserved when socket stays blocked'
96150 )
0 commit comments