Skip to content

gh-111495: Add tests for PyTuple C API #118757

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 24 commits into from
Aug 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
0e590c4
gh-111495: Add tests for PyTuple C API
rawwar Nov 1, 2023
ddf0af7
+1
skirpichev May 8, 2024
571da31
Update Lib/test/test_capi/test_tuple.py
skirpichev May 9, 2024
69d9ead
address review: more tests
skirpichev May 9, 2024
3260ef4
address review: simplify tuple_pack()
skirpichev May 9, 2024
b5b4664
address review: strict checks in tuple_set_item/setitem
skirpichev May 9, 2024
1d53b0f
address review: redo resize() tests
skirpichev May 10, 2024
3248144
Merge branch 'master' into capi-tuple-tests
skirpichev Jun 17, 2024
8451a4d
address review:
skirpichev Jun 17, 2024
b2d0eac
address review: check error for PyTuple_SetItem()
skirpichev Jun 18, 2024
a977efa
address review: fix reference leak and add tests for non-immortal obj…
skirpichev Jun 18, 2024
95d1544
Add missing unittest.main() calls
skirpichev Jun 18, 2024
14b3594
address review: add tests with NULL for setitem/set_item
skirpichev Jun 18, 2024
f9554bc
address review: fix refleak
skirpichev Jun 18, 2024
e79616f
address review: rename tuple_resize
skirpichev Jun 18, 2024
26fc5a2
address review: a helper to test if item is NULL
skirpichev Jun 18, 2024
5047d6d
address review: check no refleaks if tuple shrinks
skirpichev Jun 18, 2024
a2ea4c4
address review: test added elements on resize
skirpichev Jun 18, 2024
72aa1ed
address review: test new elements
skirpichev Jun 18, 2024
f10160c
Merge branch 'main' into capi-tuple-tests
serhiy-storchaka Aug 8, 2024
8a6d636
Merge branch 'main' into capi-tuple-tests
skirpichev Aug 9, 2024
266de84
Polish and fix some errors.
serhiy-storchaka Aug 9, 2024
190e88a
Update Modules/_testcapi/tuple.c
serhiy-storchaka Aug 9, 2024
d25eae8
Address Victor's comments.
serhiy-storchaka Aug 9, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Lib/test/test_capi/test_hash.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,7 @@ def python_hash_pointer(x):
# Py_HashPointer((void*)(uintptr_t)-1) doesn't return -1 but -2
VOID_P_MAX = -1 & (2 ** (8 * SIZEOF_VOID_P) - 1)
self.assertEqual(hash_pointer(VOID_P_MAX), -2)


if __name__ == "__main__":
unittest.main()
4 changes: 4 additions & 0 deletions Lib/test/test_capi/test_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -345,3 +345,7 @@ def test_list_extend(self):

# CRASHES list_extend(NULL, [])
# CRASHES list_extend([], NULL)


if __name__ == "__main__":
unittest.main()
4 changes: 4 additions & 0 deletions Lib/test/test_capi/test_set.py
Original file line number Diff line number Diff line change
Expand Up @@ -265,3 +265,7 @@ def test_set_next_entry(self):
with self.assertRaises(SystemError):
set_next(object(), 0)
# CRASHES: set_next(NULL, 0)


if __name__ == "__main__":
unittest.main()
4 changes: 4 additions & 0 deletions Lib/test/test_capi/test_time.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,7 @@ def test_time(self):
# Test PyTime_Time() and PyTime_TimeRaw()
self.check_clock(_testcapi.PyTime_Time, time.time)
self.check_clock(_testcapi.PyTime_TimeRaw, time.time)


if __name__ == "__main__":
unittest.main()
261 changes: 261 additions & 0 deletions Lib/test/test_capi/test_tuple.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
import unittest
import sys
from collections import namedtuple
from test.support import import_helper

_testcapi = import_helper.import_module('_testcapi')
_testlimitedcapi = import_helper.import_module('_testlimitedcapi')

NULL = None
PY_SSIZE_T_MIN = _testcapi.PY_SSIZE_T_MIN
PY_SSIZE_T_MAX = _testcapi.PY_SSIZE_T_MAX

class TupleSubclass(tuple):
pass


class CAPITest(unittest.TestCase):
def test_check(self):
# Test PyTuple_Check()
check = _testlimitedcapi.tuple_check

self.assertTrue(check((1, 2)))
self.assertTrue(check(()))
self.assertTrue(check(TupleSubclass((1, 2))))
self.assertFalse(check({1: 2}))
self.assertFalse(check([1, 2]))
self.assertFalse(check(42))
self.assertFalse(check(object()))

# CRASHES check(NULL)

def test_tuple_checkexact(self):
# Test PyTuple_CheckExact()
check = _testlimitedcapi.tuple_checkexact

self.assertTrue(check((1, 2)))
self.assertTrue(check(()))
self.assertFalse(check(TupleSubclass((1, 2))))
self.assertFalse(check({1: 2}))
self.assertFalse(check([1, 2]))
self.assertFalse(check(42))
self.assertFalse(check(object()))

# CRASHES check(NULL)

def test_tuple_new(self):
# Test PyTuple_New()
tuple_new = _testlimitedcapi.tuple_new
size = _testlimitedcapi.tuple_size
checknull = _testcapi._check_tuple_item_is_NULL

tup1 = tuple_new(0)
self.assertEqual(tup1, ())
self.assertEqual(size(tup1), 0)
self.assertIs(type(tup1), tuple)
tup2 = tuple_new(1)
self.assertIs(type(tup2), tuple)
self.assertEqual(size(tup2), 1)
self.assertIsNot(tup2, tup1)
self.assertTrue(checknull(tup2, 0))

self.assertRaises(SystemError, tuple_new, -1)
self.assertRaises(SystemError, tuple_new, PY_SSIZE_T_MIN)
self.assertRaises(MemoryError, tuple_new, PY_SSIZE_T_MAX)

def test_tuple_pack(self):
# Test PyTuple_Pack()
pack = _testlimitedcapi.tuple_pack

self.assertEqual(pack(0), ())
self.assertEqual(pack(1, [1]), ([1],))
self.assertEqual(pack(2, [1], [2]), ([1], [2]))

self.assertRaises(SystemError, pack, PY_SSIZE_T_MIN)
self.assertRaises(SystemError, pack, -1)
self.assertRaises(MemoryError, pack, PY_SSIZE_T_MAX)

# CRASHES pack(1, NULL)
# CRASHES pack(2, [1])

def test_tuple_size(self):
# Test PyTuple_Size()
size = _testlimitedcapi.tuple_size

self.assertEqual(size(()), 0)
self.assertEqual(size((1, 2)), 2)
self.assertEqual(size(TupleSubclass((1, 2))), 2)

self.assertRaises(SystemError, size, [])
self.assertRaises(SystemError, size, 42)
self.assertRaises(SystemError, size, object())

# CRASHES size(NULL)

def test_tuple_get_size(self):
# Test PyTuple_GET_SIZE()
size = _testcapi.tuple_get_size

self.assertEqual(size(()), 0)
self.assertEqual(size((1, 2)), 2)
self.assertEqual(size(TupleSubclass((1, 2))), 2)

def test_tuple_getitem(self):
# Test PyTuple_GetItem()
getitem = _testlimitedcapi.tuple_getitem

tup = ([1], [2], [3])
self.assertEqual(getitem(tup, 0), [1])
self.assertEqual(getitem(tup, 2), [3])

tup2 = TupleSubclass(([1], [2], [3]))
self.assertEqual(getitem(tup2, 0), [1])
self.assertEqual(getitem(tup2, 2), [3])

self.assertRaises(IndexError, getitem, tup, PY_SSIZE_T_MIN)
self.assertRaises(IndexError, getitem, tup, -1)
self.assertRaises(IndexError, getitem, tup, len(tup))
self.assertRaises(IndexError, getitem, tup, PY_SSIZE_T_MAX)
self.assertRaises(SystemError, getitem, [1, 2, 3], 1)
self.assertRaises(SystemError, getitem, 42, 1)

# CRASHES getitem(NULL, 0)

def test_tuple_get_item(self):
# Test PyTuple_GET_ITEM()
get_item = _testcapi.tuple_get_item

tup = ([1], [2], [3])
self.assertEqual(get_item(tup, 0), [1])
self.assertEqual(get_item(tup, 2), [3])

tup2 = TupleSubclass(([1], [2], [3]))
self.assertEqual(get_item(tup2, 0), [1])
self.assertEqual(get_item(tup2, 2), [3])

# CRASHES get_item(NULL, 0)

def test_tuple_getslice(self):
# Test PyTuple_GetSlice()
getslice = _testlimitedcapi.tuple_getslice

# empty
tup = ([1], [2], [3])
self.assertEqual(getslice(tup, PY_SSIZE_T_MIN, 0), ())
self.assertEqual(getslice(tup, -1, 0), ())
self.assertEqual(getslice(tup, 3, PY_SSIZE_T_MAX), ())
self.assertEqual(getslice(tup, 1, 1), ())
self.assertEqual(getslice(tup, 2, 1), ())
tup = TupleSubclass(([1], [2], [3]))
self.assertEqual(getslice(tup, PY_SSIZE_T_MIN, 0), ())
self.assertEqual(getslice(tup, -1, 0), ())
self.assertEqual(getslice(tup, 3, PY_SSIZE_T_MAX), ())
self.assertEqual(getslice(tup, 1, 1), ())
self.assertEqual(getslice(tup, 2, 1), ())

# slice
tup = ([1], [2], [3], [4])
self.assertEqual(getslice(tup, 1, 3), ([2], [3]))
tup = TupleSubclass(([1], [2], [3], [4]))
self.assertEqual(getslice(tup, 1, 3), ([2], [3]))

# whole
tup = ([1], [2], [3])
self.assertEqual(getslice(tup, 0, 3), tup)
self.assertEqual(getslice(tup, 0, 100), tup)
self.assertEqual(getslice(tup, -100, 100), tup)
tup = TupleSubclass(([1], [2], [3]))
self.assertEqual(getslice(tup, 0, 3), tup)
self.assertEqual(getslice(tup, 0, 100), tup)
self.assertEqual(getslice(tup, -100, 100), tup)

self.assertRaises(SystemError, getslice, [[1], [2], [3]], 0, 0)
self.assertRaises(SystemError, getslice, 42, 0, 0)

# CRASHES getslice(NULL, 0, 0)

def test_tuple_setitem(self):
# Test PyTuple_SetItem()
setitem = _testlimitedcapi.tuple_setitem
checknull = _testcapi._check_tuple_item_is_NULL

tup = ([1], [2])
self.assertEqual(setitem(tup, 0, []), ([], [2]))
self.assertEqual(setitem(tup, 1, []), ([1], []))

tup2 = setitem(tup, 1, NULL)
self.assertTrue(checknull(tup2, 1))

tup2 = TupleSubclass(([1], [2]))
self.assertRaises(SystemError, setitem, tup2, 0, [])

self.assertRaises(IndexError, setitem, tup, PY_SSIZE_T_MIN, [])
self.assertRaises(IndexError, setitem, tup, -1, [])
self.assertRaises(IndexError, setitem, tup, len(tup), [])
self.assertRaises(IndexError, setitem, tup, PY_SSIZE_T_MAX, [])
self.assertRaises(SystemError, setitem, [1], 0, [])
self.assertRaises(SystemError, setitem, 42, 0, [])

# CRASHES setitem(NULL, 0, [])

def test_tuple_set_item(self):
# Test PyTuple_SET_ITEM()
set_item = _testcapi.tuple_set_item
checknull = _testcapi._check_tuple_item_is_NULL

tup = ([1], [2])
self.assertEqual(set_item(tup, 0, []), ([], [2]))
self.assertEqual(set_item(tup, 1, []), ([1], []))

tup2 = set_item(tup, 1, NULL)
self.assertTrue(checknull(tup2, 1))

tup2 = TupleSubclass(([1], [2]))
self.assertIs(set_item(tup2, 0, []), tup2)
self.assertEqual(tup2, ([], [2]))

# CRASHES set_item(tup, -1, [])
# CRASHES set_item(tup, len(tup), [])
# CRASHES set_item([1], 0, [])
# CRASHES set_item(NULL, 0, [])

def test__tuple_resize(self):
# Test _PyTuple_Resize()
resize = _testcapi._tuple_resize
checknull = _testcapi._check_tuple_item_is_NULL

a = ()
b = resize(a, 0, False)
self.assertEqual(len(a), 0)
self.assertEqual(len(b), 0)
b = resize(a, 2, False)
self.assertEqual(len(a), 0)
self.assertEqual(len(b), 2)
self.assertTrue(checknull(b, 0))
self.assertTrue(checknull(b, 1))

a = ([1], [2], [3])
b = resize(a, 3)
self.assertEqual(b, a)
b = resize(a, 2)
self.assertEqual(b, a[:2])
b = resize(a, 5)
self.assertEqual(len(b), 5)
self.assertEqual(b[:3], a)
self.assertTrue(checknull(b, 3))
self.assertTrue(checknull(b, 4))

a = ()
self.assertRaises(MemoryError, resize, a, PY_SSIZE_T_MAX)
self.assertRaises(SystemError, resize, a, -1)
self.assertRaises(SystemError, resize, a, PY_SSIZE_T_MIN)
# refcount > 1
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i don't get how you make sure that the refcount is > 1 ? Maybe add self.assertGreater(sys.getrefcount(a), 1)?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One reference from the local variable and other from a function argument.

Actually, it is > 1 in all other tests too.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah ok, it makes sense.

a = (1, 2, 3)
self.assertRaises(SystemError, resize, a, 3, False)
self.assertRaises(SystemError, resize, a, 0, False)
# non-tuple
self.assertRaises(SystemError, resize, [1, 2, 3], 0, False)
self.assertRaises(SystemError, resize, NULL, 0, False)

if __name__ == "__main__":
unittest.main()
2 changes: 1 addition & 1 deletion Modules/Setup.stdlib.in
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@
@MODULE__TESTBUFFER_TRUE@_testbuffer _testbuffer.c
@MODULE__TESTINTERNALCAPI_TRUE@_testinternalcapi _testinternalcapi.c _testinternalcapi/test_lock.c _testinternalcapi/pytime.c _testinternalcapi/set.c _testinternalcapi/test_critical_sections.c
@MODULE__TESTCAPI_TRUE@_testcapi _testcapimodule.c _testcapi/vectorcall.c _testcapi/heaptype.c _testcapi/abstract.c _testcapi/unicode.c _testcapi/dict.c _testcapi/set.c _testcapi/list.c _testcapi/tuple.c _testcapi/getargs.c _testcapi/datetime.c _testcapi/docstring.c _testcapi/mem.c _testcapi/watchers.c _testcapi/long.c _testcapi/float.c _testcapi/complex.c _testcapi/numbers.c _testcapi/structmember.c _testcapi/exceptions.c _testcapi/code.c _testcapi/buffer.c _testcapi/pyatomic.c _testcapi/run.c _testcapi/file.c _testcapi/codec.c _testcapi/immortal.c _testcapi/gc.c _testcapi/hash.c _testcapi/time.c _testcapi/bytes.c _testcapi/object.c _testcapi/monitoring.c
@MODULE__TESTLIMITEDCAPI_TRUE@_testlimitedcapi _testlimitedcapi.c _testlimitedcapi/abstract.c _testlimitedcapi/bytearray.c _testlimitedcapi/bytes.c _testlimitedcapi/complex.c _testlimitedcapi/dict.c _testlimitedcapi/eval.c _testlimitedcapi/float.c _testlimitedcapi/heaptype_relative.c _testlimitedcapi/list.c _testlimitedcapi/long.c _testlimitedcapi/object.c _testlimitedcapi/pyos.c _testlimitedcapi/set.c _testlimitedcapi/sys.c _testlimitedcapi/unicode.c _testlimitedcapi/vectorcall_limited.c
@MODULE__TESTLIMITEDCAPI_TRUE@_testlimitedcapi _testlimitedcapi.c _testlimitedcapi/abstract.c _testlimitedcapi/bytearray.c _testlimitedcapi/bytes.c _testlimitedcapi/complex.c _testlimitedcapi/dict.c _testlimitedcapi/eval.c _testlimitedcapi/float.c _testlimitedcapi/heaptype_relative.c _testlimitedcapi/list.c _testlimitedcapi/long.c _testlimitedcapi/object.c _testlimitedcapi/pyos.c _testlimitedcapi/set.c _testlimitedcapi/sys.c _testlimitedcapi/tuple.c _testlimitedcapi/unicode.c _testlimitedcapi/vectorcall_limited.c
@MODULE__TESTCLINIC_TRUE@_testclinic _testclinic.c
@MODULE__TESTCLINIC_LIMITED_TRUE@_testclinic_limited _testclinic_limited.c

Expand Down
Loading
Loading