Skip to content

Commit 306cd11

Browse files
committed
Add buffered writer tests
1 parent 2d09746 commit 306cd11

2 files changed

Lines changed: 155 additions & 0 deletions

File tree

integration/run_python_tests.sh

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,4 +31,19 @@ for file in $(find $SCRIPT_DIR/tests/ -maxdepth 1 -type f -name "*.py"); do
3131
fi
3232
done
3333

34+
# An uncaught exception must exit non-zero, and the exit-time flush of the
35+
# buffered sys.stdout must run before the traceback is printed, so with a
36+
# redirected stdout the script's own output comes first.
37+
file=$SCRIPT_DIR/tests/expected_failures/print_then_raise.py
38+
output=$(timeout 10s $PYTHON_EXECUTABLE $file --gc-frequency $GC_FREQUENCY 2>&1)
39+
if [ $? -eq 0 ]; then
40+
echo $file "... FAILED! (expected a non-zero exit code)"
41+
exit_code=1
42+
elif [ "$(echo "$output" | head -n 1)" != "before-raise" ]; then
43+
echo $file "... FAILED! (script output must precede the traceback, got: ${output})"
44+
exit_code=1
45+
else
46+
echo $file "... PASSED!"
47+
fi
48+
3449
exit $exit_code
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
# BufferedWriter must respect buffer_size like CPython (Modules/_io/bufferedio.c):
2+
# - writes that fit in the buffer's free space are only buffered (no raw write)
3+
# - writes that don't fit first drain the buffer to the raw stream
4+
# - payloads larger than buffer_size bypass the buffer and go straight to raw
5+
# - the remaining tail (<= buffer_size) is buffered again
6+
import _io
7+
8+
PATH = "/tmp/pycpp_buffered_writer_test.bin"
9+
10+
11+
def file_contents():
12+
f = _io.FileIO(PATH, "rb")
13+
data = f.readall()
14+
f.close()
15+
return data
16+
17+
18+
# 1. a small write stays in the buffer until flush
19+
w = _io.BufferedWriter(_io.FileIO(PATH, "wb"))
20+
assert w.write(b"abcd") == 4
21+
assert file_contents() == b"", file_contents()
22+
w.flush()
23+
assert file_contents() == b"abcd", file_contents()
24+
25+
# 2. overflowing the buffer drains it, and an oversized payload bypasses the buffer
26+
w = _io.BufferedWriter(_io.FileIO(PATH, "wb"), 8)
27+
assert w.write(b"abcd") == 4
28+
assert file_contents() == b"", file_contents()
29+
assert w.write(b"efghijklm") == 9
30+
assert file_contents() == b"abcdefghijklm", file_contents()
31+
32+
# 3. a tail smaller than buffer_size stays buffered after the drain
33+
w = _io.BufferedWriter(_io.FileIO(PATH, "wb"), 8)
34+
assert w.write(b"abcdef") == 6
35+
assert w.write(b"ghi") == 3
36+
assert file_contents() == b"abcdef", file_contents()
37+
w.flush()
38+
assert file_contents() == b"abcdefghi", file_contents()
39+
40+
# 4. an exact fit is fully buffered
41+
w = _io.BufferedWriter(_io.FileIO(PATH, "wb"), 8)
42+
assert w.write(b"12345678") == 8
43+
assert file_contents() == b"", file_contents()
44+
w.flush()
45+
assert file_contents() == b"12345678", file_contents()
46+
47+
# 5. buffer_size must be strictly positive
48+
try:
49+
_io.BufferedWriter(_io.FileIO(PATH, "wb"), 0)
50+
assert False, "expected ValueError"
51+
except ValueError:
52+
pass
53+
54+
55+
# 6. raw can be any duck-typed object; a write() that over-reports the byte
56+
# count must raise OSError instead of wrapping the unsigned byte counters
57+
class OverReportingWriter:
58+
def write(self, b):
59+
return 1000
60+
61+
62+
w = _io.BufferedWriter(OverReportingWriter(), 8)
63+
try:
64+
w.write(b"0123456789abcdef")
65+
assert False, "expected OSError"
66+
except OSError:
67+
pass
68+
69+
70+
# 7. same for a negative count
71+
class NegativeWriter:
72+
def write(self, b):
73+
return -1
74+
75+
76+
w = _io.BufferedWriter(NegativeWriter(), 8)
77+
try:
78+
w.write(b"0123456789abcdef")
79+
assert False, "expected OSError"
80+
except OSError:
81+
pass
82+
83+
# 8. a raw write() returning 0 makes no progress; retrying forever would hang,
84+
# so it must raise OSError
85+
class ZeroWriter:
86+
def write(self, b):
87+
return 0
88+
89+
90+
w = _io.BufferedWriter(ZeroWriter(), 8)
91+
try:
92+
w.write(b"0123456789abcdef")
93+
assert False, "expected OSError"
94+
except OSError:
95+
pass
96+
97+
98+
# 9. a raw write() returning None means the stream accepted no data without
99+
# blocking; that is an OSError (BlockingIOError in CPython), not a crash
100+
class NoneWriter:
101+
def write(self, b):
102+
return None
103+
104+
105+
w = _io.BufferedWriter(NoneWriter(), 8)
106+
try:
107+
w.write(b"0123456789abcdef")
108+
assert False, "expected OSError"
109+
except OSError:
110+
pass
111+
112+
113+
# 10. any other non-int return from raw write() is a TypeError, not a crash
114+
class StringWriter:
115+
def write(self, b):
116+
return "16"
117+
118+
119+
w = _io.BufferedWriter(StringWriter(), 8)
120+
try:
121+
w.write(b"0123456789abcdef")
122+
assert False, "expected TypeError"
123+
except TypeError:
124+
pass
125+
126+
# 11. an object created via __new__ without __init__ has no raw stream; every
127+
# I/O method must raise ValueError instead of dereferencing it
128+
uninitialized = _io.BufferedWriter.__new__(_io.BufferedWriter)
129+
for method in (
130+
uninitialized.isatty,
131+
uninitialized.flush,
132+
lambda: uninitialized.write(b"x"),
133+
):
134+
try:
135+
method()
136+
assert False, "expected ValueError"
137+
except ValueError:
138+
pass
139+
140+
print("buffered_writer: ok")

0 commit comments

Comments
 (0)