Skip to content

gh-124570: ctypes: Run some Structure tests on Union as well #124976

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 1 commit into from
Oct 10, 2024
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
353 changes: 353 additions & 0 deletions Lib/test/test_ctypes/test_structunion.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,353 @@
"""Common tests for ctypes.Structure and ctypes.Union"""

import unittest
from ctypes import (Structure, Union, POINTER, sizeof, alignment,
c_char, c_byte, c_ubyte,
c_short, c_ushort, c_int, c_uint,
c_long, c_ulong, c_longlong, c_ulonglong, c_float, c_double)
from ._support import (_CData, PyCStructType, UnionType,
Py_TPFLAGS_DISALLOW_INSTANTIATION,
Py_TPFLAGS_IMMUTABLETYPE)
from struct import calcsize


class StructUnionTestBase:
formats = {"c": c_char,
"b": c_byte,
"B": c_ubyte,
"h": c_short,
"H": c_ushort,
"i": c_int,
"I": c_uint,
"l": c_long,
"L": c_ulong,
"q": c_longlong,
"Q": c_ulonglong,
"f": c_float,
"d": c_double,
}

def test_subclass(self):
class X(self.cls):
_fields_ = [("a", c_int)]

class Y(X):
_fields_ = [("b", c_int)]

class Z(X):
pass

self.assertEqual(sizeof(X), sizeof(c_int))
self.check_sizeof(Y,
struct_size=sizeof(c_int)*2,
union_size=sizeof(c_int))
self.assertEqual(sizeof(Z), sizeof(c_int))
self.assertEqual(X._fields_, [("a", c_int)])
self.assertEqual(Y._fields_, [("b", c_int)])
self.assertEqual(Z._fields_, [("a", c_int)])

def test_subclass_delayed(self):
class X(self.cls):
pass
self.assertEqual(sizeof(X), 0)
X._fields_ = [("a", c_int)]

class Y(X):
pass
self.assertEqual(sizeof(Y), sizeof(X))
Y._fields_ = [("b", c_int)]

class Z(X):
pass

self.assertEqual(sizeof(X), sizeof(c_int))
self.check_sizeof(Y,
struct_size=sizeof(c_int)*2,
union_size=sizeof(c_int))
self.assertEqual(sizeof(Z), sizeof(c_int))
self.assertEqual(X._fields_, [("a", c_int)])
self.assertEqual(Y._fields_, [("b", c_int)])
self.assertEqual(Z._fields_, [("a", c_int)])

def test_inheritance_hierarchy(self):
self.assertEqual(self.cls.mro(), [self.cls, _CData, object])
self.assertEqual(type(self.metacls), type)

def test_type_flags(self):
for cls in self.cls, self.metacls:
with self.subTest(cls=cls):
self.assertTrue(cls.__flags__ & Py_TPFLAGS_IMMUTABLETYPE)
self.assertFalse(cls.__flags__ & Py_TPFLAGS_DISALLOW_INSTANTIATION)

def test_metaclass_details(self):
# Abstract classes (whose metaclass __init__ was not called) can't be
# instantiated directly
NewClass = self.metacls.__new__(self.metacls, 'NewClass',
(self.cls,), {})
for cls in self.cls, NewClass:
with self.subTest(cls=cls):
with self.assertRaisesRegex(TypeError, "abstract class"):
obj = cls()

# Cannot call the metaclass __init__ more than once
class T(self.cls):
_fields_ = [("x", c_char),
("y", c_char)]
with self.assertRaisesRegex(SystemError, "already initialized"):
self.metacls.__init__(T, 'ptr', (), {})

def test_alignment(self):
class X(self.cls):
_fields_ = [("x", c_char * 3)]
self.assertEqual(alignment(X), calcsize("s"))
self.assertEqual(sizeof(X), calcsize("3s"))

class Y(self.cls):
_fields_ = [("x", c_char * 3),
("y", c_int)]
self.assertEqual(alignment(Y), alignment(c_int))
self.check_sizeof(Y,
struct_size=calcsize("3s i"),
union_size=max(calcsize("3s"), calcsize("i")))

class SI(self.cls):
_fields_ = [("a", X),
("b", Y)]
self.assertEqual(alignment(SI), max(alignment(Y), alignment(X)))
self.check_sizeof(SI,
struct_size=calcsize("3s0i 3si 0i"),
union_size=max(calcsize("3s"), calcsize("i")))

class IS(self.cls):
_fields_ = [("b", Y),
("a", X)]

self.assertEqual(alignment(SI), max(alignment(X), alignment(Y)))
self.check_sizeof(IS,
struct_size=calcsize("3si 3s 0i"),
union_size=max(calcsize("3s"), calcsize("i")))

class XX(self.cls):
_fields_ = [("a", X),
("b", X)]
self.assertEqual(alignment(XX), alignment(X))
self.check_sizeof(XX,
struct_size=calcsize("3s 3s 0s"),
union_size=calcsize("3s"))

def test_empty(self):
# I had problems with these
#
# Although these are pathological cases: Empty Structures!
class X(self.cls):
_fields_ = []

# Is this really the correct alignment, or should it be 0?
self.assertTrue(alignment(X) == 1)
self.assertTrue(sizeof(X) == 0)

class XX(self.cls):
_fields_ = [("a", X),
("b", X)]

self.assertEqual(alignment(XX), 1)
self.assertEqual(sizeof(XX), 0)

def test_fields(self):
# test the offset and size attributes of Structure/Union fields.
class X(self.cls):
_fields_ = [("x", c_int),
("y", c_char)]

self.assertEqual(X.x.offset, 0)
self.assertEqual(X.x.size, sizeof(c_int))

if self.cls == Structure:
self.assertEqual(X.y.offset, sizeof(c_int))
else:
self.assertEqual(X.y.offset, 0)
self.assertEqual(X.y.size, sizeof(c_char))

# readonly
self.assertRaises((TypeError, AttributeError), setattr, X.x, "offset", 92)
self.assertRaises((TypeError, AttributeError), setattr, X.x, "size", 92)

# XXX Should we check nested data types also?
# offset is always relative to the class...

def test_invalid_field_types(self):
class POINT(self.cls):
pass
self.assertRaises(TypeError, setattr, POINT, "_fields_", [("x", 1), ("y", 2)])

def test_invalid_name(self):
# field name must be string
def declare_with_name(name):
class S(self.cls):
_fields_ = [(name, c_int)]

self.assertRaises(TypeError, declare_with_name, b"x")

def test_intarray_fields(self):
class SomeInts(self.cls):
_fields_ = [("a", c_int * 4)]

# can use tuple to initialize array (but not list!)
self.assertEqual(SomeInts((1, 2)).a[:], [1, 2, 0, 0])
self.assertEqual(SomeInts((1, 2)).a[::], [1, 2, 0, 0])
self.assertEqual(SomeInts((1, 2)).a[::-1], [0, 0, 2, 1])
self.assertEqual(SomeInts((1, 2)).a[::2], [1, 0])
self.assertEqual(SomeInts((1, 2)).a[1:5:6], [2])
self.assertEqual(SomeInts((1, 2)).a[6:4:-1], [])
self.assertEqual(SomeInts((1, 2, 3, 4)).a[:], [1, 2, 3, 4])
self.assertEqual(SomeInts((1, 2, 3, 4)).a[::], [1, 2, 3, 4])
# too long
# XXX Should raise ValueError?, not RuntimeError
self.assertRaises(RuntimeError, SomeInts, (1, 2, 3, 4, 5))

def test_huge_field_name(self):
# issue12881: segfault with large structure field names
def create_class(length):
class S(self.cls):
_fields_ = [('x' * length, c_int)]

for length in [10 ** i for i in range(0, 8)]:
try:
create_class(length)
except MemoryError:
# MemoryErrors are OK, we just don't want to segfault
pass

def test_abstract_class(self):
class X(self.cls):
_abstract_ = "something"
with self.assertRaisesRegex(TypeError, r"^abstract class$"):
X()

def test_methods(self):
self.assertIn("in_dll", dir(type(self.cls)))
self.assertIn("from_address", dir(type(self.cls)))
self.assertIn("in_dll", dir(type(self.cls)))


class StructureTestCase(unittest.TestCase, StructUnionTestBase):
cls = Structure
metacls = PyCStructType

def test_metaclass_name(self):
self.assertEqual(self.metacls.__name__, "PyCStructType")

def check_sizeof(self, cls, *, struct_size, union_size):
self.assertEqual(sizeof(cls), struct_size)

def test_simple_structs(self):
for code, tp in self.formats.items():
class X(Structure):
_fields_ = [("x", c_char),
("y", tp)]
self.assertEqual((sizeof(X), code),
(calcsize("c%c0%c" % (code, code)), code))


class UnionTestCase(unittest.TestCase, StructUnionTestBase):
cls = Union
metacls = UnionType

def test_metaclass_name(self):
self.assertEqual(self.metacls.__name__, "UnionType")

def check_sizeof(self, cls, *, struct_size, union_size):
self.assertEqual(sizeof(cls), union_size)

def test_simple_unions(self):
for code, tp in self.formats.items():
class X(Union):
_fields_ = [("x", c_char),
("y", tp)]
self.assertEqual((sizeof(X), code),
(calcsize("%c" % (code)), code))


class PointerMemberTestBase:
def test(self):
# a Structure/Union with a POINTER field
class S(self.cls):
_fields_ = [("array", POINTER(c_int))]

s = S()
# We can assign arrays of the correct type
s.array = (c_int * 3)(1, 2, 3)
items = [s.array[i] for i in range(3)]
self.assertEqual(items, [1, 2, 3])

s.array[0] = 42

items = [s.array[i] for i in range(3)]
self.assertEqual(items, [42, 2, 3])

s.array[0] = 1

items = [s.array[i] for i in range(3)]
self.assertEqual(items, [1, 2, 3])

class PointerMemberTestCase_Struct(unittest.TestCase, PointerMemberTestBase):
cls = Structure

def test_none_to_pointer_fields(self):
class S(self.cls):
_fields_ = [("x", c_int),
("p", POINTER(c_int))]

s = S()
s.x = 12345678
s.p = None
self.assertEqual(s.x, 12345678)

class PointerMemberTestCase_Union(unittest.TestCase, PointerMemberTestBase):
cls = Union

def test_none_to_pointer_fields(self):
class S(self.cls):
_fields_ = [("x", c_int),
("p", POINTER(c_int))]

s = S()
s.x = 12345678
s.p = None
self.assertFalse(s.p) # NULL pointers are falsy


class TestRecursiveBase:
def test_contains_itself(self):
class Recursive(self.cls):
pass

try:
Recursive._fields_ = [("next", Recursive)]
except AttributeError as details:
self.assertIn("Structure or union cannot contain itself",
str(details))
else:
self.fail("Structure or union cannot contain itself")


def test_vice_versa(self):
class First(self.cls):
pass
class Second(self.cls):
pass

First._fields_ = [("second", Second)]

try:
Second._fields_ = [("first", First)]
except AttributeError as details:
self.assertIn("_fields_ is final", str(details))
else:
self.fail("AttributeError not raised")

class TestRecursiveStructure(unittest.TestCase, TestRecursiveBase):
cls = Structure

class TestRecursiveUnion(unittest.TestCase, TestRecursiveBase):
cls = Union
Loading
Loading