Skip to content

gh-110525: Add tests for internal set CAPI #110630

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 4 commits into from
Oct 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
54 changes: 52 additions & 2 deletions Lib/test/test_capi/test_set.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@

from test.support import import_helper

# Skip this test if the _testcapi module isn't available.
# Skip this test if the _testcapi or _testinternalcapi modules aren't available.
_testcapi = import_helper.import_module('_testcapi')
_testinternalcapi = import_helper.import_module('_testinternalcapi')

class set_subclass(set):
pass
Expand All @@ -12,13 +13,15 @@ class frozenset_subclass(frozenset):
pass


class TestSetCAPI(unittest.TestCase):
class BaseSetTests:
def assertImmutable(self, action, *args):
self.assertRaises(SystemError, action, frozenset(), *args)
self.assertRaises(SystemError, action, frozenset({1}), *args)
self.assertRaises(SystemError, action, frozenset_subclass(), *args)
self.assertRaises(SystemError, action, frozenset_subclass({1}), *args)


class TestSetCAPI(BaseSetTests, unittest.TestCase):
def test_set_check(self):
check = _testcapi.set_check
self.assertTrue(check(set()))
Expand Down Expand Up @@ -213,3 +216,50 @@ def test_clear(self):
clear(object())
self.assertImmutable(clear)
# CRASHES: clear(NULL)


class TestInternalCAPI(BaseSetTests, unittest.TestCase):
def test_set_update(self):
update = _testinternalcapi.set_update
for cls in (set, set_subclass):
for it in ('ab', ('a', 'b'), ['a', 'b'],
set('ab'), set_subclass('ab'),
frozenset('ab'), frozenset_subclass('ab')):
with self.subTest(cls=cls, it=it):
instance = cls()
self.assertEqual(update(instance, it), 0)
self.assertEqual(instance, {'a', 'b'})
instance = cls(it)
self.assertEqual(update(instance, it), 0)
self.assertEqual(instance, {'a', 'b'})
with self.assertRaisesRegex(TypeError, 'object is not iterable'):
update(cls(), 1)
with self.assertRaisesRegex(TypeError, "unhashable type: 'dict'"):
update(cls(), [{}])
with self.assertRaises(SystemError):
update(object(), 'ab')
self.assertImmutable(update, 'ab')
# CRASHES: update(NULL, object())
# CRASHES: update(instance, NULL)
# CRASHES: update(NULL, NULL)

def test_set_next_entry(self):
set_next = _testinternalcapi.set_next_entry
for cls in (set, set_subclass, frozenset, frozenset_subclass):
with self.subTest(cls=cls):
instance = cls('abc')
pos = 0
items = []
while True:
res = set_next(instance, pos)
if res is None:
break
rc, pos, hash_, item = res
items.append(item)
self.assertEqual(rc, 1)
self.assertIn(item, instance)
self.assertEqual(hash(item), hash_)
self.assertEqual(items, list(instance))
with self.assertRaises(SystemError):
set_next(object(), 0)
# CRASHES: set_next(NULL, 0)
2 changes: 1 addition & 1 deletion Modules/Setup.stdlib.in
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@
@MODULE_XXSUBTYPE_TRUE@xxsubtype xxsubtype.c
@MODULE__XXTESTFUZZ_TRUE@_xxtestfuzz _xxtestfuzz/_xxtestfuzz.c _xxtestfuzz/fuzzer.c
@MODULE__TESTBUFFER_TRUE@_testbuffer _testbuffer.c
@MODULE__TESTINTERNALCAPI_TRUE@_testinternalcapi _testinternalcapi.c _testinternalcapi/test_lock.c _testinternalcapi/pytime.c
@MODULE__TESTINTERNALCAPI_TRUE@_testinternalcapi _testinternalcapi.c _testinternalcapi/test_lock.c _testinternalcapi/pytime.c _testinternalcapi/set.c
@MODULE__TESTCAPI_TRUE@_testcapi _testcapimodule.c _testcapi/vectorcall.c _testcapi/vectorcall_limited.c _testcapi/heaptype.c _testcapi/abstract.c _testcapi/unicode.c _testcapi/dict.c _testcapi/set.c _testcapi/getargs.c _testcapi/datetime.c _testcapi/docstring.c _testcapi/mem.c _testcapi/watchers.c _testcapi/long.c _testcapi/float.c _testcapi/structmember.c _testcapi/exceptions.c _testcapi/code.c _testcapi/buffer.c _testcapi/pyatomic.c _testcapi/pyos.c _testcapi/immortal.c _testcapi/heaptype_relative.c _testcapi/gc.c
@MODULE__TESTCLINIC_TRUE@_testclinic _testclinic.c
@MODULE__TESTCLINIC_LIMITED_TRUE@_testclinic_limited _testclinic_limited.c
Expand Down
3 changes: 3 additions & 0 deletions Modules/_testinternalcapi.c
Original file line number Diff line number Diff line change
Expand Up @@ -1602,6 +1602,9 @@ module_exec(PyObject *module)
if (_PyTestInternalCapi_Init_PyTime(module) < 0) {
return 1;
}
if (_PyTestInternalCapi_Init_Set(module) < 0) {
return 1;
}

if (PyModule_Add(module, "SIZEOF_PYGC_HEAD",
PyLong_FromSsize_t(sizeof(PyGC_Head))) < 0) {
Expand Down
1 change: 1 addition & 0 deletions Modules/_testinternalcapi/parts.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,6 @@

int _PyTestInternalCapi_Init_Lock(PyObject *module);
int _PyTestInternalCapi_Init_PyTime(PyObject *module);
int _PyTestInternalCapi_Init_Set(PyObject *module);

#endif // Py_TESTINTERNALCAPI_PARTS_H
59 changes: 59 additions & 0 deletions Modules/_testinternalcapi/set.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#include "parts.h"
#include "../_testcapi/util.h" // NULLABLE, RETURN_INT

#include "pycore_setobject.h"


static PyObject *
set_update(PyObject *self, PyObject *args)
{
PyObject *set, *iterable;
if (!PyArg_ParseTuple(args, "OO", &set, &iterable)) {
return NULL;
}
NULLABLE(set);
NULLABLE(iterable);
RETURN_INT(_PySet_Update(set, iterable));
}

static PyObject *
set_next_entry(PyObject *self, PyObject *args)
{
int rc;
Py_ssize_t pos;
Py_hash_t hash = (Py_hash_t)UNINITIALIZED_SIZE;
PyObject *set, *item = UNINITIALIZED_PTR;
if (!PyArg_ParseTuple(args, "On", &set, &pos)) {
return NULL;
}
NULLABLE(set);

rc = _PySet_NextEntry(set, &pos, &item, &hash);
if (rc == 1) {
Copy link
Member

Choose a reason for hiding this comment

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

What if it returns 2 or -2?

Suggested change
if (rc == 1) {
if (rc != 0 && rc != -1) {

Copy link
Member Author

Choose a reason for hiding this comment

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

Sorry, I don't understand this one. Right now it is defined as:

cpython/Objects/setobject.c

Lines 2332 to 2346 in 66a9b10

int
_PySet_NextEntry(PyObject *set, Py_ssize_t *pos, PyObject **key, Py_hash_t *hash)
{
setentry *entry;
if (!PyAnySet_Check(set)) {
PyErr_BadInternalCall();
return -1;
}
if (set_next((PySetObject *)set, pos, &entry) == 0)
return 0;
*key = entry->key;
*hash = entry->hash;
return 1;
}

It cannot return anything except [-1, 0, 1].
Do you mean that it can return something other than [-1, 0, 1] in the future?
I think that our test case must catch this change and be adapted if needed.

This function is not documented currently.

Copy link
Member

@serhiy-storchaka serhiy-storchaka Oct 10, 2023

Choose a reason for hiding this comment

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

The purpose of the test is to verify our assumptions (that it cannot return anything except [-1, 0, 1]). Otherwise there would not be need of tests.

In future a new return can be added in the code, or refactoring can lead to returning non-initialized variable in rare case. The wrapper will successfully return None.

Copy link
Member Author

Choose a reason for hiding this comment

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

Got it, I used an assertion, which is more readable in my opinion 👍

return Py_BuildValue("innO", rc, pos, hash, item);
}
assert(item == UNINITIALIZED_PTR);
assert(hash == (Py_hash_t)UNINITIALIZED_SIZE);
if (rc == -1) {
return NULL;
}
assert(rc == 0);
Py_RETURN_NONE;
}


static PyMethodDef TestMethods[] = {
{"set_update", set_update, METH_VARARGS},
{"set_next_entry", set_next_entry, METH_VARARGS},

{NULL},
};

int
_PyTestInternalCapi_Init_Set(PyObject *m)
{
if (PyModule_AddFunctions(m, TestMethods) < 0) {
return -1;
}
return 0;
}
1 change: 1 addition & 0 deletions PCbuild/_testinternalcapi.vcxproj
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@
<ClCompile Include="..\Modules\_testinternalcapi.c" />
<ClCompile Include="..\Modules\_testinternalcapi\pytime.c" />
<ClCompile Include="..\Modules\_testinternalcapi\test_lock.c" />
<ClCompile Include="..\Modules\_testinternalcapi\set.c" />
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="..\PC\python_nt.rc" />
Expand Down