Skip to content

Commit 1ebc13b

Browse files
authored
[3.15] gh-157242: Add more PyBytesWriter tests (#159546)
* gh-157242: Add more PyBytesWriter tests (#157352) * Fix PyBytesWriter_Grow() check on the size argument. * Fix undefined behavior in _PyBytes_ResizeKeepOnError(): raise OverflowError on integer overflow. * Add write(), grow() and get_data() methods to the _testcapi wrapper of PyBytesWriter. * Remove the string argument from _testcapi.PyBytesWriter() and resize() method. * Add tests on PyBytesWriter_Resize() invalid sizes. (cherry picked from commit 63d4398) * gh-157242: Allow negative grow in PyBytesWriter_Grow() (#157375) PyBytesWriter_Resize() doesn't need to call byteswriter_resize() if the buffer is already large enough. (cherry picked from commit bab97c5) * gh-157242: Add byteswriter_check_consistency() (#157723) Add byteswriter_check_consistency() to check that the bytes/bytearray object can be mutated. It is called by all PyBytesWriter functions. * Add test_thread to PyBytesWriter tests. * _PyBytes_IsMutable() now checks _PyObject_IsUniquelyReferenced(). * Clarify thread safety in PyBytesWriter documentation. (cherry picked from commit e2ff498) * Run prek --all-files
1 parent a3bb662 commit 1ebc13b

4 files changed

Lines changed: 393 additions & 86 deletions

File tree

‎Doc/c-api/bytes.rst‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -299,8 +299,8 @@ object.
299299
300300
A bytes writer instance.
301301
302-
The API is **not thread safe**: a writer should only be used by a single
303-
thread at the same time.
302+
The API is **not thread safe**. To share a writer with multiple threads, a
303+
critical section or a lock is needed.
304304
305305
The instance must be destroyed by :c:func:`PyBytesWriter_Finish` on
306306
success, or :c:func:`PyBytesWriter_Discard` on error.
@@ -436,7 +436,7 @@ Low-level API
436436
On success, return ``0``.
437437
On error, set an exception and return ``-1``.
438438
439-
*size* can be negative to shrink the writer.
439+
*grow* can be negative to shrink the writer.
440440
441441
.. c:function:: void* PyBytesWriter_GrowAndUpdatePointer(PyBytesWriter *writer, Py_ssize_t size, void *buf)
442442

‎Lib/test/test_capi/test_bytes.py‎

Lines changed: 191 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import sys
2+
import threading
23
import unittest
34
from test import support
45
from test.support import import_helper
6+
from test.support import threading_helper
57

68
_testlimitedcapi = import_helper.import_module('_testlimitedcapi')
79
_testcapi = import_helper.import_module('_testcapi')
@@ -318,6 +320,10 @@ def test_join(self):
318320

319321
class BaseWriterTest:
320322
RESULT_TYPE = NotImplementedError
323+
SMALL_BUFFER = 11 # bytes
324+
assert SMALL_BUFFER < _testcapi.PyBytesWriter_small_buffer
325+
LARGE_BUFFER = _testcapi.PyBytesWriter_small_buffer + 17 # bytes
326+
NEW_BYTE = b'\xff'
321327

322328
def create_writer(self, alloc=0, string=b''):
323329
raise NotImplementedError
@@ -334,23 +340,60 @@ def test_create(self):
334340
self.assertEqual(writer.get_size(), 0)
335341
self.bytes_equal(writer.finish(), b'')
336342

337-
writer = self.create_writer(3, b'abc')
343+
writer = self.create_writer(3)
344+
writer.write(0, b'abc')
338345
self.assertEqual(writer.get_size(), 3)
339346
self.bytes_equal(writer.finish(), b'abc')
340347

348+
@unittest.skipUnless(support.Py_DEBUG, 'need Py_DEBUG')
349+
def test_get_data(self):
350+
# Test PyBytesWriter_GetData()
351+
writer = self.create_writer(6)
352+
NEW_BYTE = self.NEW_BYTE
353+
self.assertEqual(writer.get_data(), NEW_BYTE * 6)
354+
writer.write(0, b'abc')
355+
self.assertEqual(writer.get_data(), b'abc' + NEW_BYTE * 3)
356+
writer.write(3, b'123')
357+
self.assertEqual(writer.get_data(), b'abc123')
358+
359+
# Switch from small buffer to large buffer
360+
small, large = self.SMALL_BUFFER, self.LARGE_BUFFER
361+
writer = self.create_writer(small)
362+
self.assertEqual(writer.get_data(), NEW_BYTE * small)
363+
writer.write(0, b's' * small)
364+
self.assertEqual(writer.get_data(), b's' * small)
365+
writer.resize(large)
366+
self.assertEqual(writer.get_data(), b's' * small + NEW_BYTE * (large - small))
367+
writer.write(small, b'L' * (large - small))
368+
self.assertEqual(writer.get_data(), b's' * small + b'L' * (large - small))
369+
370+
# Resize large buffer
371+
small, large = self.SMALL_BUFFER, self.LARGE_BUFFER
372+
writer = self.create_writer(large)
373+
self.assertEqual(writer.get_data(), NEW_BYTE * large)
374+
writer.write(0, b'L' * large)
375+
self.assertEqual(writer.get_data(), b'L' * large)
376+
writer.resize(large + 10)
377+
self.assertEqual(writer.get_data(), b'L' * large + NEW_BYTE * 10)
378+
writer.write(large, b'#' * 10)
379+
self.assertEqual(writer.get_data(), b'L' * large + b'#' * 10)
380+
341381
def test_finish_with_size(self):
342382
# Test PyBytesWriter_FinishWithSize()
343-
writer = self.create_writer(10, b'abcdef')
383+
writer = self.create_writer(10)
384+
writer.write(0, b'abc123')
344385
self.assertEqual(writer.get_size(), 10)
345386
self.bytes_equal(writer.finish_with_size(3), b'abc')
346387

347388
# Error if the size is negative
348-
writer = self.create_writer(3, b'abc')
389+
writer = self.create_writer(3, )
390+
writer.write(0, b'abc')
349391
with self.assertRaises(ValueError):
350392
writer.finish_with_size(-3)
351393

352394
# Error if the requested size is larger than the allocated size
353-
writer = self.create_writer(3, b'abc')
395+
writer = self.create_writer(3)
396+
writer.write(0, b'abc')
354397
with self.assertRaises(ValueError):
355398
writer.finish_with_size(4)
356399

@@ -368,44 +411,118 @@ def test_write_bytes(self):
368411
def test_resize(self):
369412
# Test PyBytesWriter_Resize()
370413
writer = self.create_writer()
371-
writer.resize(len(b'number=123456'), b'number=123456')
372-
writer.resize(len(b'number=123456'), b'')
373-
self.assertEqual(writer.get_size(), len(b'number=123456'))
374-
self.bytes_equal(writer.finish(), b'number=123456')
414+
writer.resize(len(b'hello'))
415+
writer.write(0, b'hello')
416+
self.assertEqual(writer.finish(), b'hello')
375417

376418
writer = self.create_writer()
377-
writer.resize(0, b'')
378-
writer.resize(len(b'number=123456'), b'number=123456')
379-
self.bytes_equal(writer.finish(), b'number=123456')
380-
419+
writer.resize(0) # noop
420+
writer.resize(len(b'number'))
421+
writer.write(0, b'number')
422+
writer.resize(len(b'number='))
423+
writer.write(len(b'number'), b'=')
424+
writer.resize(len(b'number=123'), )
425+
writer.write(len(b'number='), b'123')
426+
writer.resize(len(b'number=123')) # noop
427+
self.assertEqual(writer.finish(), b'number=123')
428+
429+
# Switch from small buffer to large buffer
381430
writer = self.create_writer()
382-
writer.resize(len(b'number='), b'number=')
383-
writer.resize(len(b'number=123456'), b'123456')
384-
self.bytes_equal(writer.finish(), b'number=123456')
385-
431+
small, large = self.SMALL_BUFFER, self.LARGE_BUFFER
432+
writer.resize(small)
433+
writer.write(0, b's' * small)
434+
writer.resize(large)
435+
writer.write(small, b'L' * (large - small))
436+
self.assertEqual(writer.finish(),
437+
b's' * small + b'L' * (large - small))
438+
439+
# Make sure that it's possible to write after a resize to zero
440+
# when a bytes/bytearray object is allocated.
386441
writer = self.create_writer()
387-
writer.resize(len(b'number='), b'number=')
388-
writer.resize(len(b'number='), b'')
389-
writer.resize(len(b'number=123456'), b'123456')
390-
self.bytes_equal(writer.finish(), b'number=123456')
442+
writer.resize(self.LARGE_BUFFER)
443+
writer.resize(0)
444+
writer.write_bytes(b'abc', 3)
445+
self.assertEqual(writer.finish(), b'abc')
446+
447+
# invalid size
448+
for size in (self.SMALL_BUFFER, self.LARGE_BUFFER):
449+
with self.subTest(size=size):
450+
writer = self.create_writer()
451+
writer.write_bytes(b'x' * size, -1)
452+
with self.assertRaisesRegex(ValueError, 'size must be >= 0'):
453+
writer.resize(-1)
454+
with self.assertRaises((MemoryError, OverflowError)):
455+
writer.resize(_testcapi.PY_SSIZE_T_MAX)
456+
self.assertEqual(writer.finish(), b'x' * size)
457+
458+
def test_grow(self):
459+
# Test PyBytesWriter_Grow()
460+
writer = self.create_writer(0)
461+
writer.grow(len(b'number=123'))
462+
writer.write(0, b'number=123')
463+
self.assertEqual(writer.finish(), b'number=123')
391464

392465
writer = self.create_writer()
393-
writer.resize(len(b'number'), b'number')
394-
writer.resize(len(b'number='), b'=')
395-
writer.resize(len(b'number=123'), b'123')
396-
writer.resize(len(b'number=123456'), b'456')
397-
self.bytes_equal(writer.finish(), b'number=123456')
466+
writer.grow(0) # noop
467+
writer.grow(len(b'number'))
468+
writer.write(0, b'number')
469+
writer.grow(len(b'='))
470+
writer.write(len(b'number'), b'=')
471+
writer.grow(len(b'123'), )
472+
writer.write(len(b'number='), b'123')
473+
writer.grow(0) # noop
474+
self.assertEqual(writer.finish(), b'number=123')
475+
476+
for size in (self.SMALL_BUFFER, self.LARGE_BUFFER):
477+
with self.subTest(size=size):
478+
# Truncate the last byte
479+
data = b'x' * size
480+
writer = self.create_writer(size)
481+
writer.write(0, data)
482+
self.assertEqual(writer.get_data(), data)
483+
writer.grow(-1)
484+
self.assertEqual(writer.get_data(), data[:-1])
485+
self.assertEqual(writer.finish(), data[:-1])
486+
487+
# Make the buffer empty
488+
writer = self.create_writer(size)
489+
writer.write(0, data)
490+
writer.grow(-size)
491+
self.assertEqual(writer.get_data(), b'')
492+
self.assertEqual(writer.finish(), b'')
493+
494+
# Switch from small buffer to large buffer
495+
writer = self.create_writer()
496+
small, large = self.SMALL_BUFFER, self.LARGE_BUFFER
497+
writer.grow(small)
498+
writer.write(0, b's' * small)
499+
writer.grow(large - small)
500+
writer.write(small, b'L' * (large - small))
501+
self.assertEqual(writer.finish(),
502+
b's' * small + b'L' * (large - small))
503+
504+
# invalid size
505+
for size in (self.SMALL_BUFFER, self.LARGE_BUFFER):
506+
with self.subTest(size=size):
507+
writer = self.create_writer()
508+
writer.write_bytes(b'x' * size, -1)
509+
with self.assertRaisesRegex(ValueError, 'invalid size'):
510+
writer.grow(-size - 1)
511+
with self.assertRaises(MemoryError):
512+
writer.grow(_testcapi.PY_SSIZE_T_MAX)
513+
self.assertEqual(writer.finish(), b'x' * size)
398514

399515
@support.nomemtest
400516
def test_resize_error(self):
401-
small_buffer = _testcapi.PyBytesWriter_small_buffer
402-
init = b'x' * (small_buffer * 2)
403-
writer = self.create_writer(len(init), init)
517+
# Test PyBytesWriter_Resize() error
518+
init = b'x' * self.LARGE_BUFFER
519+
writer = self.create_writer(len(init))
520+
writer.write(0, init)
404521
size = len(init) + 100
405522
try:
406523
with self.assertRaises(MemoryError):
407524
_testcapi.set_nomemory(0)
408-
writer.resize(size, b'')
525+
writer.resize(size)
409526
finally:
410527
_testcapi.remove_mem_hooks()
411528
suffix = b'still working'
@@ -443,36 +560,71 @@ def test_format_s_memory_error(self):
443560
finally:
444561
_testcapi.remove_mem_hooks()
445562
writer.write_bytes(b'after', -1)
446-
self.bytes_equal(writer.finish(), b'after')
563+
564+
@threading_helper.requires_working_threading()
565+
def test_thread(self):
566+
# PyBytesWriter can be used by multiple threads: it's up to the caller
567+
# to implement a lock to prevent concurrent accesses.
568+
writer = self.create_writer(0)
569+
size = None
570+
data = None
571+
572+
def thread_func(writer, LARGE_BUFFER):
573+
nonlocal size, data
574+
575+
# create a bytes object for the buffer
576+
writer.write_bytes(b'x' * LARGE_BUFFER, LARGE_BUFFER)
577+
578+
# so we can check a write with a bytes object
579+
writer.write_bytes(b'yz', 2)
580+
writer.format_i(b'i=%i', 5)
581+
writer.resize(10)
582+
data = writer.get_data()
583+
size = writer.get_size()
584+
585+
thread = threading.Thread(target=thread_func,
586+
args=(writer, self.LARGE_BUFFER))
587+
thread.start()
588+
threading_helper.join_thread(thread)
589+
590+
self.assertEqual(size, 10)
591+
self.assertEqual(data, b'x' * 10)
592+
self.assertEqual(writer.finish(), b'x' * 10)
447593

448594

449595
class BytesWriterTest(BaseWriterTest, unittest.TestCase):
450596
RESULT_TYPE = bytes
451597

452-
def create_writer(self, alloc=0, string=b''):
598+
def create_writer(self, size=0):
453599
# Test PyBytesWriter_Create()
454-
return _testcapi.PyBytesWriter(alloc, string, 0)
600+
return _testcapi.PyBytesWriter(size, 0)
455601

456602
# Only PyBytesWriter_Create() returns singletons
457603
def test_singletons(self):
458604
empty = b''
459605
singletons = {ch: bytes((ch,)) for ch in range(256)}
460-
small_buffer = _testcapi.PyBytesWriter_small_buffer
461606

462607
writer = self.create_writer()
463608
self.assertIs(writer.finish(), empty)
464609

465-
# Test writer larger than small_buffer
610+
# Large buffer
466611
writer = self.create_writer()
467-
unused_text = b'x' * (small_buffer * 2)
612+
unused_text = b'x' * self.LARGE_BUFFER
468613
writer.write_bytes(unused_text, len(unused_text))
469614
self.assertIs(writer.finish_with_size(0), empty)
470615

616+
# Large buffer with resize
617+
writer = self.create_writer()
618+
unused_text = b'x' * self.LARGE_BUFFER
619+
writer.write_bytes(unused_text, len(unused_text))
620+
writer.resize(0)
621+
self.assertIs(writer.finish(), empty)
622+
471623
for ch in range(256):
472-
text = bytes((ch,))
624+
byte = bytes((ch,))
473625

474626
writer = self.create_writer()
475-
writer.write_bytes(text, 1)
627+
writer.write_bytes(byte, len(byte))
476628
self.assertIs(writer.finish(), singletons[ch])
477629

478630
def test_example_abc(self):
@@ -488,9 +640,9 @@ def test_example_highlevel(self):
488640
class ByteArrayWriterTest(BaseWriterTest, unittest.TestCase):
489641
RESULT_TYPE = bytearray
490642

491-
def create_writer(self, alloc=0, string=b''):
643+
def create_writer(self, size=0):
492644
# Test private _PyBytesWriter_CreateByteArray()
493-
return _testcapi.PyBytesWriter(alloc, string, 1)
645+
return _testcapi.PyBytesWriter(size, 1)
494646

495647

496648
if __name__ == "__main__":

0 commit comments

Comments
 (0)