Skip to content

Commit

Permalink
gh-124570: ctypes: Run some Structure tests on Union as well (GH-124976)
Browse files Browse the repository at this point in the history
- Move some Structure tests to test_structunion; use a common base
  test class + two subclasses to run them on Union too
- Remove test_union for now as it's redundant

Note: `test_simple_structs` & `test_simple_unions` are in the common
file because they share `formats`.
  • Loading branch information
encukou authored Oct 10, 2024
1 parent c914212 commit 01fc3b3
Show file tree
Hide file tree
Showing 3 changed files with 368 additions and 357 deletions.
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

0 comments on commit 01fc3b3

Please sign in to comment.