diff --git a/test/scripts/alignment_test.py b/test/scripts/alignment_test.py index b714b5e..3e59160 100644 --- a/test/scripts/alignment_test.py +++ b/test/scripts/alignment_test.py @@ -308,3 +308,55 @@ class s_t(struct): s = s_t.from_bytes(memory) self.assertEqual(s.a.value, 0x41) self.assertEqual(s.flags.value, 5) + + +class AlignedStructTailPaddingInstanceTest(unittest.TestCase): + """Instance size must include tail padding for aligned structs.""" + + def test_instance_size_matches_class_size(self): + """size_of(instance) should equal size_of(class) for aligned structs.""" + class aligned_t(struct): + _aligned_ = True + a: c_int + b: c_char + + self.assertEqual(size_of(aligned_t), 8) + + memory = pystruct.pack("", result) + + +# ---------- union.py coverage ---------- + + +class UnionEmptyTest(unittest.TestCase): + """Empty union edge cases.""" + + def test_empty_union_get(self): + u = union(None, None, 4) + self.assertIsNone(u.get()) + + def test_empty_union_to_dict(self): + u = union(None, None, 4) + self.assertIsNone(u.to_dict()) + + def test_empty_union_to_str(self): + u = union(None, None, 4) + self.assertEqual(u.to_str(), "union(empty)") + + +class UnionSetNoVariantTest(unittest.TestCase): + """Setting a union without an active variant raises RuntimeError.""" + + def test_set_raises(self): + u = union(None, None, 4) + with self.assertRaises(RuntimeError): + u._set(42) + + +class UnionResetNoFreezeTest(unittest.TestCase): + """Resetting an unfrozen union raises RuntimeError.""" + + def test_reset_raises(self): + u = union(None, None, 4) + with self.assertRaises(RuntimeError): + u.reset() + + +class UnionNoneResolverTest(unittest.TestCase): + """Union with None resolver returns zero bytes.""" + + def test_to_bytes_none_resolver(self): + u = union(None, None, 8) + self.assertEqual(u.to_bytes(), b"\x00" * 8) + + def test_freeze_none_resolver(self): + u = union(None, None, 4) + u.freeze() + self.assertEqual(u._frozen_bytes, b"\x00" * 4) + + +class PlainUnionDiffTest(unittest.TestCase): + """Plain union diff returns per-variant diffs.""" + + def test_plain_union_diff(self): + class s_t(struct): + data: union = union_of({"i": c_int, "l": c_long}) + + memory = bytearray(8) + pystruct.pack_into(" offset 12 + memory[12:16] = (20).to_bytes(4, "little") + memory[16:24] = (0).to_bytes(8, "little") # next -> null + + node = Node.from_bytes(memory) + self.assertEqual(node.val.value, 10) + next_node = node.next.unwrap() + self.assertEqual(next_node.val.value, 20) + + +class PtrToStrFieldTest(unittest.TestCase): + """ptr to_str with Field-backed wrapper.""" + + def test_ptr_to_field_wrapper(self): + """ptr_to(c_int) creates a Field-backed wrapper that has a qualified name.""" + memory = bytearray(16) + memory[0:8] = (8).to_bytes(8, "little") + memory[8:12] = (42).to_bytes(4, "little") + + lib = inflater(memory) + p = lib.inflate(ptr_to(c_int), 0) + result = p.to_str() + self.assertIn("0x8", result) + + +# ---------- forward_ref_inflater.py coverage ---------- + + +class LazyPtrFieldUnresolvableTest(unittest.TestCase): + """_LazyPtrField when forward ref cannot be resolved → returns raw ptr.""" + + def test_unresolvable_forward_ref_returns_raw_ptr(self): + """ptr['NonExistentType'] should still inflate, just as an untyped ptr.""" + from typing import ForwardRef + from libdestruct.common.forward_ref_inflater import _LazyPtrField + + lazy = _LazyPtrField(ForwardRef("CompletelyBogusTypeThatDoesNotExist"), owner=None) + memory = bytearray(8) + memory[0:8] = (0).to_bytes(8, "little") + result = lazy.inflate(MemoryResolver(memory, 0)) + self.assertIsInstance(result, ptr) + # wrapper should be None since it couldn't resolve + self.assertIsNone(result.wrapper) + + def test_forward_ref_resolves_to_non_type(self): + """Forward ref that eval's to a non-type value returns None from _resolve_forward_ref.""" + from typing import ForwardRef + from libdestruct.common.forward_ref_inflater import _LazyPtrField + + # "42" eval's to int 42, not a type + lazy = _LazyPtrField(ForwardRef("42"), owner=None) + result = lazy._resolve_forward_ref() + self.assertIsNone(result) + + def test_forward_ref_eval_exception(self): + """Forward ref that raises during eval returns None.""" + from typing import ForwardRef + from libdestruct.common.forward_ref_inflater import _LazyPtrField + + # Valid syntax but unresolvable name → NameError during eval + lazy = _LazyPtrField(ForwardRef("NoSuchTypeAnywhere"), owner=None) + result = lazy._resolve_forward_ref() + self.assertIsNone(result) + + +class SubscriptedPtrHandlerEdgeCasesTest(unittest.TestCase): + """_subscripted_ptr_handler edge cases.""" + + def test_ptr_subscript_none_target(self): + """ptr[()] with empty args → untyped ptr field.""" + from libdestruct.common.forward_ref_inflater import _subscripted_ptr_handler + + result = _subscripted_ptr_handler(ptr, (), owner=None) + self.assertIsNotNone(result) + # Should return a PtrField.inflate bound method + memory = bytearray(8) + p = result(MemoryResolver(memory, 0)) + self.assertIsInstance(p, ptr) + self.assertIsNone(p.wrapper) + + def test_ptr_subscript_string_target(self): + """ptr['SomeString'] goes through ForwardRef path.""" + from libdestruct.common.forward_ref_inflater import _subscripted_ptr_handler + + result = _subscripted_ptr_handler(ptr, ("NonExistentType",), owner=None) + self.assertIsNotNone(result) + # Should return a _LazyPtrField.inflate bound method + memory = bytearray(8) + p = result(MemoryResolver(memory, 0)) + self.assertIsInstance(p, ptr) + + def test_ptr_subscript_non_type_non_string_target(self): + """ptr[42] (invalid target) → fallback untyped ptr.""" + from libdestruct.common.forward_ref_inflater import _subscripted_ptr_handler + + result = _subscripted_ptr_handler(ptr, (42,), owner=None) + self.assertIsNotNone(result) + memory = bytearray(8) + p = result(MemoryResolver(memory, 0)) + self.assertIsInstance(p, ptr) + self.assertIsNone(p.wrapper) + + def test_ptr_subscript_concrete_type(self): + """ptr[c_int] resolves immediately to typed ptr.""" + from libdestruct.common.forward_ref_inflater import _subscripted_ptr_handler + + result = _subscripted_ptr_handler(ptr, (c_int,), owner=None) + self.assertIsNotNone(result) + memory = bytearray(16) + memory[0:8] = (8).to_bytes(8, "little") + memory[8:12] = (42).to_bytes(4, "little") + p = result(MemoryResolver(memory, 0)) + self.assertIsInstance(p, ptr) + self.assertIsNotNone(p.wrapper) + + +class BareForwardRefInflaterTest(unittest.TestCase): + """_forward_ref_inflater for bare ForwardRef annotations.""" + + def test_ptr_forward_ref_string_parsing(self): + """ForwardRef('ptr[SomeType]') is parsed and creates a lazy ptr.""" + from typing import ForwardRef + from libdestruct.common.forward_ref_inflater import _forward_ref_inflater + + ref = ForwardRef("ptr['Node']") + result = _forward_ref_inflater(ref, type(None), owner=None) + self.assertIsNotNone(result) + # Should return a _LazyPtrField.inflate bound method + memory = bytearray(8) + p = result(MemoryResolver(memory, 0)) + self.assertIsInstance(p, ptr) + + def test_ptr_forward_ref_double_quoted(self): + """ForwardRef('ptr[\"Node\"]') with double quotes.""" + from typing import ForwardRef + from libdestruct.common.forward_ref_inflater import _forward_ref_inflater + + ref = ForwardRef('ptr["Node"]') + result = _forward_ref_inflater(ref, type(None), owner=None) + self.assertIsNotNone(result) + memory = bytearray(8) + p = result(MemoryResolver(memory, 0)) + self.assertIsInstance(p, ptr) + + def test_ptr_forward_ref_unquoted(self): + """ForwardRef('ptr[c_int]') with unquoted inner type.""" + from typing import ForwardRef + from libdestruct.common.forward_ref_inflater import _forward_ref_inflater + + ref = ForwardRef("ptr[c_int]") + result = _forward_ref_inflater(ref, type(None), owner=None) + self.assertIsNotNone(result) + memory = bytearray(8) + p = result(MemoryResolver(memory, 0)) + self.assertIsInstance(p, ptr) + + def test_non_ptr_forward_ref_raises(self): + """ForwardRef('SomeRandomThing') raises ValueError.""" + from typing import ForwardRef + from libdestruct.common.forward_ref_inflater import _forward_ref_inflater + + ref = ForwardRef("SomeRandomThing") + with self.assertRaises(ValueError) as ctx: + _forward_ref_inflater(ref, type(None), owner=None) + self.assertIn("SomeRandomThing", str(ctx.exception)) + + def test_lazy_ptr_with_owner_resolves(self): + """_LazyPtrField with owner tuple resolves types from owner's module.""" + from typing import ForwardRef + from libdestruct.common.forward_ref_inflater import _LazyPtrField + + # Create a lazy field that references c_int (available in this module's globals) + lazy = _LazyPtrField(ForwardRef("c_int"), owner=(None, type(self))) + resolved = lazy._resolve_forward_ref() + self.assertIs(resolved, c_int) + + +# ---------- flags int_flag_field.py coverage ---------- + + +class FlagsFieldSizesTest(unittest.TestCase): + """IntFlagField with different sizes.""" + + class Perms(IntFlag): + R = 1 + W = 2 + X = 4 + + def test_flags_size_1(self): + from libdestruct import flags_of + + class s_t(struct): + perms: c_int = flags_of(self.Perms, size=1) + + memory = (7).to_bytes(1, "little") + s = s_t.from_bytes(memory) + self.assertEqual(s.perms.value, self.Perms.R | self.Perms.W | self.Perms.X) + + def test_flags_size_2(self): + from libdestruct import flags_of + + class s_t(struct): + perms: c_int = flags_of(self.Perms, size=2) + + memory = (3).to_bytes(2, "little") + s = s_t.from_bytes(memory) + self.assertEqual(s.perms.value, self.Perms.R | self.Perms.W) + + def test_flags_size_8(self): + from libdestruct import flags_of + + class s_t(struct): + perms: c_int = flags_of(self.Perms, size=8) + + memory = (1).to_bytes(8, "little") + s = s_t.from_bytes(memory) + self.assertEqual(s.perms.value, self.Perms.R) + + def test_flags_invalid_size(self): + from libdestruct import flags_of + with self.assertRaises(ValueError): + flags_of(self.Perms, size=3) + + def test_flags_size_too_large(self): + from libdestruct import flags_of + with self.assertRaises(ValueError): + flags_of(self.Perms, size=9) + + +# ---------- c_str.py coverage ---------- + + +class CStrEdgeCasesTest(unittest.TestCase): + """c_str edge cases.""" + + def test_repr(self): + from libdestruct import c_str + + memory = bytearray(b"Hello\x00") + lib = inflater(memory) + s = lib.inflate(c_str, 0) + r = repr(s) + self.assertIsInstance(r, str) + + def test_negative_index_raises(self): + from libdestruct import c_str + + memory = bytearray(b"Hello\x00") + lib = inflater(memory) + s = lib.inflate(c_str, 0) + with self.assertRaises(IndexError): + s.get(-2) + + def test_set_negative_index_raises(self): + from libdestruct import c_str + + memory = bytearray(b"Hello\x00") + lib = inflater(memory) + s = lib.inflate(c_str, 0) + with self.assertRaises(IndexError): + s._set(b"X", -2) + + def test_set_full_string(self): + from libdestruct import c_str + + memory = bytearray(b"Hello\x00") + lib = inflater(memory) + s = lib.inflate(c_str, 0) + s.value = b"World" + self.assertEqual(s.value, b"World") + + +# ---------- struct_parser.py coverage ---------- + + +class StructParserEdgeCasesTest(unittest.TestCase): + """C parser edge cases for coverage.""" + + def test_enum_in_struct_raises(self): + """Enum inside struct is not yet supported, must raise TypeError.""" + from libdestruct.c.struct_parser import definition_to_type + + with self.assertRaises(TypeError): + definition_to_type("struct test { enum { A, B, C } val; };") + + def test_struct_with_pointer_member(self): + from libdestruct.c.struct_parser import definition_to_type + + t = definition_to_type("struct test { int *p; int x; };") + self.assertIn("p", t.__annotations__) + self.assertIn("x", t.__annotations__) + + def test_struct_with_array_read(self): + from libdestruct.c.struct_parser import definition_to_type + + t = definition_to_type("struct test { int arr[3]; int x; };") + memory = b"".join((i).to_bytes(4, "little") for i in [10, 20, 30, 42]) + s = t.from_bytes(memory) + self.assertEqual(s.x.value, 42) + + def test_named_struct_cached(self): + """Named structs are cached and reusable.""" + from libdestruct.c.struct_parser import definition_to_type, clear_parser_cache + + clear_parser_cache() + t = definition_to_type(""" + struct Inner { int x; }; + struct Outer { struct Inner a; int b; }; + """) + memory = (1).to_bytes(4, "little") + (2).to_bytes(4, "little") + s = t.from_bytes(memory) + self.assertEqual(s.b.value, 2) + clear_parser_cache() + + +if __name__ == "__main__": + unittest.main() diff --git a/test/scripts/ctypes_test.py b/test/scripts/ctypes_integration_test.py similarity index 100% rename from test/scripts/ctypes_test.py rename to test/scripts/ctypes_integration_test.py diff --git a/test/scripts/endianness_test.py b/test/scripts/endianness_test.py index 72f53fd..b8aead0 100644 --- a/test/scripts/endianness_test.py +++ b/test/scripts/endianness_test.py @@ -264,5 +264,34 @@ class s_t(struct): self.assertEqual(memory, pystruct.pack(">I", 5)) +class EndiannessValidationTest(unittest.TestCase): + """inflater() should reject invalid endianness strings.""" + + def test_invalid_endianness_raises(self): + """Passing a typo like 'big-endian' must raise ValueError, not silently produce wrong results.""" + with self.assertRaises(ValueError): + inflater(bytearray(4), endianness="big-endian") + + def test_invalid_endianness_typo(self): + """A random typo must raise ValueError.""" + with self.assertRaises(ValueError): + inflater(bytearray(4), endianness="typo") + + def test_valid_endianness_big(self): + """'big' is accepted without error.""" + lib = inflater(bytearray(4), endianness="big") + self.assertIsNotNone(lib) + + def test_valid_endianness_little(self): + """'little' is accepted without error.""" + lib = inflater(bytearray(4), endianness="little") + self.assertIsNotNone(lib) + + def test_from_bytes_invalid_endianness(self): + """from_bytes with invalid endianness must raise ValueError.""" + with self.assertRaises(ValueError): + c_int.from_bytes(b"\x00\x00\x00\x00", endianness="big-endian") + + if __name__ == "__main__": unittest.main() diff --git a/test/scripts/enum_test.py b/test/scripts/enum_test.py index bd283ac..6807a27 100644 --- a/test/scripts/enum_test.py +++ b/test/scripts/enum_test.py @@ -8,6 +8,8 @@ from enum import Enum, IntEnum from libdestruct import inflater, c_int, enum, enum_of, struct +from libdestruct.backing.memory_resolver import MemoryResolver +from libdestruct.common.enum.enum import enum as ld_enum class EnumTest(unittest.TestCase): def test_enum(self): @@ -131,3 +133,51 @@ class test_t(struct): result = bytes(test) self.assertIsInstance(result, bytes) + + +class EnumLenientSetTest(unittest.TestCase): + """enum._set must handle raw ints from lenient mode without crashing.""" + + def test_set_raw_int_from_lenient_get(self): + """Setting back a raw int obtained from lenient get() should work.""" + class Color(IntEnum): + RED = 0 + GREEN = 1 + + memory = bytearray((99).to_bytes(4, "little")) + e = ld_enum(MemoryResolver(memory, 0), Color, c_int, lenient=True) + + val = e.get() + self.assertEqual(val, 99) + self.assertIsInstance(val, int) + self.assertNotIsInstance(val, IntEnum) + + e.value = val + self.assertEqual(e.get(), 99) + + def test_set_enum_member_still_works(self): + """Setting a valid enum member should still work.""" + class Color(IntEnum): + RED = 0 + GREEN = 1 + + memory = bytearray(4) + e = ld_enum(MemoryResolver(memory, 0), Color, c_int, lenient=True) + + e.value = Color.GREEN + self.assertEqual(e.get(), Color.GREEN) + + def test_reset_after_freeze_with_unknown_value(self): + """freeze() + reset() with unknown enum value should not crash.""" + class Color(IntEnum): + RED = 0 + GREEN = 1 + + memory = bytearray((99).to_bytes(4, "little")) + e = ld_enum(MemoryResolver(memory, 0), Color, c_int, lenient=True) + + e.freeze() + memory[0:4] = (0).to_bytes(4, "little") + + e.reset() + self.assertEqual(e.get(), 99) diff --git a/test/scripts/resolver_unit_test.py b/test/scripts/resolver_test.py similarity index 81% rename from test/scripts/resolver_unit_test.py rename to test/scripts/resolver_test.py index 7c3a54e..2fdba16 100644 --- a/test/scripts/resolver_unit_test.py +++ b/test/scripts/resolver_test.py @@ -4,11 +4,13 @@ # Licensed under the MIT license. See LICENSE file in the project root for details. # +import inspect import unittest from libdestruct import c_int, c_str, c_uint, inflater, struct, ptr, ptr_to_self from libdestruct.backing.fake_resolver import FakeResolver from libdestruct.backing.memory_resolver import MemoryResolver +from libdestruct.backing.resolver import Resolver class FakeResolverTest(unittest.TestCase): @@ -107,5 +109,22 @@ def test_c_uint_write_to_bytearray(self): self.assertEqual(obj.value, 0xDEADBEEF) +class ResolverParameterNameTest(unittest.TestCase): + """Resolver method parameter names must match documentation.""" + + def test_resolve_parameter_name_is_index(self): + """Resolver.resolve second parameter should be 'index', not 'offset'.""" + sig = inspect.signature(Resolver.resolve) + params = list(sig.parameters.keys()) + self.assertEqual(params[2], "index") + + def test_relative_from_own_parameter_names(self): + """Resolver.relative_from_own parameters should be address_offset, index_offset.""" + sig = inspect.signature(Resolver.relative_from_own) + params = list(sig.parameters.keys()) + self.assertEqual(params[1], "address_offset") + self.assertEqual(params[2], "index_offset") + + if __name__ == "__main__": unittest.main() diff --git a/test/scripts/review_fix_test.py b/test/scripts/review_fix_test.py deleted file mode 100644 index 606f1ba..0000000 --- a/test/scripts/review_fix_test.py +++ /dev/null @@ -1,209 +0,0 @@ -# -# This file is part of libdestruct (https://github.com/mrindeciso/libdestruct). -# Copyright (c) 2026 Roberto Alessandro Bertolini. All rights reserved. -# Licensed under the MIT license. See LICENSE file in the project root for details. -# - -"""Tests that expose bugs found during code review of the dev branch.""" - -import struct as pystruct -import unittest - -from libdestruct import c_float, c_double, c_int, c_long, inflater, struct -from libdestruct.common.union import union, union_of - - -class EndiannessValidationTest(unittest.TestCase): - """inflater() should reject invalid endianness strings.""" - - def test_invalid_endianness_raises(self): - """Passing a typo like 'big-endian' must raise ValueError, not silently produce wrong results.""" - with self.assertRaises(ValueError): - inflater(bytearray(4), endianness="big-endian") - - def test_invalid_endianness_typo(self): - """A random typo must raise ValueError.""" - with self.assertRaises(ValueError): - inflater(bytearray(4), endianness="typo") - - def test_valid_endianness_big(self): - """'big' is accepted without error.""" - lib = inflater(bytearray(4), endianness="big") - self.assertIsNotNone(lib) - - def test_valid_endianness_little(self): - """'little' is accepted without error.""" - lib = inflater(bytearray(4), endianness="little") - self.assertIsNotNone(lib) - - def test_from_bytes_invalid_endianness(self): - """from_bytes with invalid endianness must raise ValueError.""" - with self.assertRaises(ValueError): - c_int.from_bytes(b"\x00\x00\x00\x00", endianness="big-endian") - - -class StructAttributeCollisionTest(unittest.TestCase): - """Struct members named after internal attributes must not break core methods.""" - - def test_struct_with_frozen_field_to_bytes(self): - """A struct with a field named '_frozen' must still serialize correctly after freeze.""" - # This field name collides with obj._frozen used in to_bytes() - class s_t(struct): - _frozen: c_int - b: c_int - - memory = b"" - memory += (10).to_bytes(4, "little") - memory += (20).to_bytes(4, "little") - - s = s_t.from_bytes(memory) - # to_bytes must return the correct serialized data, not crash - self.assertEqual(s.to_bytes(), memory) - - def test_struct_with_frozen_field_hexdump(self): - """A struct with a '_frozen' field must still produce a hexdump.""" - class s_t(struct): - _frozen: c_int - - s = s_t.from_bytes((42).to_bytes(4, "little")) - # hexdump must not crash - dump = s.hexdump() - self.assertIn("2a", dump) - - def test_struct_with_members_field_eq(self): - """A struct with a field named '_members' must still support equality.""" - class s_t(struct): - _members: c_int - - a = s_t.from_bytes((1).to_bytes(4, "little")) - b = s_t.from_bytes((1).to_bytes(4, "little")) - self.assertEqual(a, b) - - def test_struct_with_members_field_to_dict(self): - """A struct with a field named '_members' must still support to_dict.""" - class s_t(struct): - _members: c_int - - s = s_t.from_bytes((5).to_bytes(4, "little")) - d = s.to_dict() - self.assertEqual(d["_members"], 5) - - def test_struct_with_frozen_struct_bytes_field(self): - """A field named '_frozen_struct_bytes' must not break freeze/to_bytes.""" - class s_t(struct): - _frozen_struct_bytes: c_int - - memory = (99).to_bytes(4, "little") - s = s_t.from_bytes(memory) - self.assertEqual(s.to_bytes(), memory) - - -class UnionGetAttrSafetyTest(unittest.TestCase): - """union.__getattr__ must produce clear errors, not internal AttributeError.""" - - def test_missing_attribute_error_message(self): - """Accessing a nonexistent attribute on a union should mention the attribute name, not '_variants'.""" - u = union(None, None, 4) - with self.assertRaises(AttributeError) as ctx: - _ = u.nonexistent_attr - # The error message must mention the user's attribute, not internal implementation details - self.assertIn("nonexistent_attr", str(ctx.exception)) - self.assertNotIn("_variants", str(ctx.exception)) - - def test_getattr_after_del_variants(self): - """Even if _variants is somehow missing, __getattr__ should not expose internal details.""" - u = union(None, None, 4) - del u.__dict__["_variants"] - with self.assertRaises(AttributeError) as ctx: - _ = u.something - self.assertIn("something", str(ctx.exception)) - - -class FloatDuplicationRegressionTest(unittest.TestCase): - """After refactoring c_float/c_double to a shared base, core behavior must be preserved.""" - - def test_c_float_read_write(self): - memory = bytearray(4) - lib = inflater(memory) - f = lib.inflate(c_float, 0) - f.value = 3.14 - self.assertAlmostEqual(f.value, 3.14, places=5) - - def test_c_double_read_write(self): - memory = bytearray(8) - lib = inflater(memory) - d = lib.inflate(c_double, 0) - d.value = 2.718281828 - self.assertAlmostEqual(d.value, 2.718281828, places=8) - - def test_c_float_freeze_diff_reset(self): - memory = bytearray(4) - lib = inflater(memory) - f = lib.inflate(c_float, 0) - f.value = 1.5 - f.freeze() - self.assertAlmostEqual(f.value, 1.5, places=5) - with self.assertRaises(ValueError): - f.value = 2.0 - - def test_c_double_freeze_diff_reset(self): - memory = bytearray(8) - lib = inflater(memory) - d = lib.inflate(c_double, 0) - d.value = 1.5 - d.freeze() - self.assertAlmostEqual(d.value, 1.5, places=5) - with self.assertRaises(ValueError): - d.value = 2.0 - - def test_c_float_from_bytes(self): - data = pystruct.pack("f", 3.14) - f = c_float.from_bytes(original, endianness="big") - self.assertAlmostEqual(f.value, 3.14, places=5) - self.assertEqual(f.to_bytes(), original) - - def test_c_double_big_endian(self): - original = pystruct.pack(">d", 2.718) - d = c_double.from_bytes(original, endianness="big") - self.assertAlmostEqual(d.value, 2.718, places=3) - self.assertEqual(d.to_bytes(), original) - - -if __name__ == "__main__": - unittest.main() diff --git a/test/scripts/review_fix_test_2.py b/test/scripts/review_fix_test_2.py deleted file mode 100644 index bf09ce3..0000000 --- a/test/scripts/review_fix_test_2.py +++ /dev/null @@ -1,289 +0,0 @@ -# -# This file is part of libdestruct (https://github.com/mrindeciso/libdestruct). -# Copyright (c) 2026 Roberto Alessandro Bertolini. All rights reserved. -# Licensed under the MIT license. See LICENSE file in the project root for details. -# - -"""Tests that expose bugs found during the second-pass code review.""" - -import unittest - -from libdestruct import ( - array, - bitfield_of, - c_int, - c_uint, - inflater, - size_of, - struct, -) -from libdestruct.c.struct_parser import clear_parser_cache, definition_to_type - - -class ComparisonOperatorSafetyTest(unittest.TestCase): - """Comparison operators must not raise TypeError for incompatible obj types.""" - - def test_lt_primitive_vs_struct_returns_not_implemented(self): - """c_int < struct should return NotImplemented, not raise TypeError.""" - class s_t(struct): - x: c_int - - memory = bytearray(4) - lib = inflater(memory) - val = lib.inflate(c_int, 0) - s = lib.inflate(s_t, 0) - - # Must not raise TypeError - result = val.__lt__(s) - self.assertIs(result, NotImplemented) - - def test_gt_primitive_vs_struct_returns_not_implemented(self): - class s_t(struct): - x: c_int - - memory = bytearray(4) - lib = inflater(memory) - val = lib.inflate(c_int, 0) - s = lib.inflate(s_t, 0) - - result = val.__gt__(s) - self.assertIs(result, NotImplemented) - - def test_le_primitive_vs_struct_returns_not_implemented(self): - class s_t(struct): - x: c_int - - memory = bytearray(4) - lib = inflater(memory) - val = lib.inflate(c_int, 0) - s = lib.inflate(s_t, 0) - - result = val.__le__(s) - self.assertIs(result, NotImplemented) - - def test_ge_primitive_vs_struct_returns_not_implemented(self): - class s_t(struct): - x: c_int - - memory = bytearray(4) - lib = inflater(memory) - val = lib.inflate(c_int, 0) - s = lib.inflate(s_t, 0) - - result = val.__ge__(s) - self.assertIs(result, NotImplemented) - - def test_eq_primitive_vs_struct_returns_not_implemented(self): - class s_t(struct): - x: c_int - - memory = bytearray(4) - lib = inflater(memory) - val = lib.inflate(c_int, 0) - s = lib.inflate(s_t, 0) - - result = val.__eq__(s) - self.assertIs(result, NotImplemented) - - def test_ne_primitive_vs_struct_returns_not_implemented(self): - class s_t(struct): - x: c_int - - memory = bytearray(4) - lib = inflater(memory) - val = lib.inflate(c_int, 0) - s = lib.inflate(s_t, 0) - - result = val.__ne__(s) - self.assertIs(result, NotImplemented) - - def test_lt_between_compatible_primitives_works(self): - """Comparisons between compatible primitives should still work.""" - memory = bytearray(8) - lib = inflater(memory) - a = lib.inflate(c_int, 0) - b = lib.inflate(c_int, 4) - a.value = 1 - b.value = 2 - - self.assertTrue(a < b) - self.assertFalse(b < a) - - def test_comparison_with_raw_int(self): - memory = bytearray(4) - lib = inflater(memory) - a = lib.inflate(c_int, 0) - a.value = 5 - - self.assertTrue(a < 10) - self.assertTrue(a > 2) - self.assertTrue(a <= 5) - self.assertTrue(a >= 5) - - -class NegativeArrayCountTest(unittest.TestCase): - """array[T, N] must reject non-positive counts at handler time.""" - - def test_negative_count_raises(self): - """array[c_int, -5] must raise ValueError.""" - with self.assertRaises(ValueError): - class s_t(struct): - data: array[c_int, -5] - # Force size computation - size_of(s_t) - - def test_zero_count_raises(self): - """array[c_int, 0] must raise ValueError.""" - with self.assertRaises(ValueError): - class s_t(struct): - data: array[c_int, 0] - size_of(s_t) - - def test_positive_count_works(self): - """array[c_int, 3] must work fine.""" - class s_t(struct): - data: array[c_int, 3] - self.assertEqual(size_of(s_t), 12) - - -class BitfieldFreezeSafetyTest(unittest.TestCase): - """Frozen bitfields must reject writes even for non-owners.""" - - def test_non_owner_bitfield_rejects_write_after_freeze(self): - """The second bitfield in a group (non-owner) must reject writes when frozen.""" - class s_t(struct): - a: c_uint = bitfield_of(c_uint, 1) - b: c_uint = bitfield_of(c_uint, 1) - - memory = bytearray(4) - lib = inflater(memory) - s = lib.inflate(s_t, 0) - - s.a.value = 1 - s.b.value = 1 - - # Freeze the entire struct (which freezes all members) - s.freeze() - - # Both bitfields should reject writes - with self.assertRaises(ValueError): - s.a.value = 0 - - with self.assertRaises(ValueError): - s.b.value = 0 - - def test_individually_frozen_non_owner_rejects_write(self): - """Freezing a non-owner bitfield individually must also reject writes.""" - class s_t(struct): - a: c_uint = bitfield_of(c_uint, 1) - b: c_uint = bitfield_of(c_uint, 1) - - memory = bytearray(4) - lib = inflater(memory) - s = lib.inflate(s_t, 0) - - s.b.value = 1 - - # Freeze only the non-owner bitfield b - s.b.freeze() - - with self.assertRaises(ValueError): - s.b.value = 0 - - -class TypeRegistryDeduplicationTest(unittest.TestCase): - """Repeated handler registration must not accumulate duplicates.""" - - def test_generic_handler_not_duplicated(self): - """Registering the same handler twice must not produce duplicate entries.""" - from libdestruct.common.type_registry import TypeRegistry - - registry = TypeRegistry() - - class DummyType: - pass - - def dummy_handler(item, args, owner): - return None - - initial_count = len(registry.generic_handlers.get(DummyType, [])) - - registry.register_generic_handler(DummyType, dummy_handler) - registry.register_generic_handler(DummyType, dummy_handler) - - count = len(registry.generic_handlers[DummyType]) - self.assertEqual(count, initial_count + 1) - - def test_instance_handler_not_duplicated(self): - """Registering the same instance handler twice must not produce duplicate entries.""" - from libdestruct.common.type_registry import TypeRegistry - - registry = TypeRegistry() - - class DummyField: - pass - - def dummy_handler(item, annotation, owner): - return None - - initial_count = len(registry.instance_handlers.get(DummyField, [])) - - registry.register_instance_handler(DummyField, dummy_handler) - registry.register_instance_handler(DummyField, dummy_handler) - - count = len(registry.instance_handlers[DummyField]) - self.assertEqual(count, initial_count + 1) - - def test_type_handler_not_duplicated(self): - """Registering the same type handler twice must not produce duplicate entries.""" - from libdestruct.common.type_registry import TypeRegistry - - registry = TypeRegistry() - - class DummyParent: - pass - - def dummy_handler(item): - return None - - initial_count = len(registry.type_handlers.get(DummyParent, [])) - - registry.register_type_handler(DummyParent, dummy_handler) - registry.register_type_handler(DummyParent, dummy_handler) - - count = len(registry.type_handlers[DummyParent]) - self.assertEqual(count, initial_count + 1) - - -class ForwardTypedefTest(unittest.TestCase): - """Forward typedef references are a known parser limitation.""" - - def setUp(self): - clear_parser_cache() - - def tearDown(self): - clear_parser_cache() - - def test_chained_typedefs_in_order(self): - """Chained typedefs in declaration order must work.""" - t = definition_to_type(""" - typedef unsigned int u32; - typedef u32 mytype; - struct S { mytype x; }; - """) - data = (42).to_bytes(4, "little") - s = t.from_bytes(data) - self.assertEqual(s.x.value, 42) - - def test_forward_typedef_reference_raises(self): - """Forward typedef reference (use before define) must raise a clear error, not crash.""" - with self.assertRaises((ValueError, TypeError)): - definition_to_type(""" - typedef mytype1 mytype2; - typedef unsigned int mytype1; - struct S { mytype2 x; }; - """) - - -if __name__ == "__main__": - unittest.main() diff --git a/test/scripts/string_test.py b/test/scripts/string_integration_test.py similarity index 100% rename from test/scripts/string_test.py rename to test/scripts/string_integration_test.py diff --git a/test/scripts/struct_parser_unit_test.py b/test/scripts/struct_parser_test.py similarity index 57% rename from test/scripts/struct_parser_unit_test.py rename to test/scripts/struct_parser_test.py index 3593306..376fe2b 100644 --- a/test/scripts/struct_parser_unit_test.py +++ b/test/scripts/struct_parser_test.py @@ -7,7 +7,7 @@ import struct as pystruct import unittest -from libdestruct.c.struct_parser import definition_to_type +from libdestruct.c.struct_parser import clear_parser_cache, definition_to_type, PARSED_STRUCTS from libdestruct import inflater @@ -88,5 +88,61 @@ def test_typedef_inflate_and_read(self): self.assertEqual(s.y.value, -42) +class AnonymousStructCacheTest(unittest.TestCase): + """Anonymous structs must not pollute the parser cache with a None key.""" + + def test_none_key_not_in_cache(self): + """Anonymous struct should not pollute PARSED_STRUCTS with a None key.""" + PARSED_STRUCTS.clear() + + definition_to_type("struct { int x; };") + + self.assertNotIn(None, PARSED_STRUCTS) + + +class UnsizedArrayMemberTest(unittest.TestCase): + """Parser must handle flexible array members (e.g. int data[]) gracefully.""" + + def test_unsized_array_member(self): + """Parsing a struct with a flexible array member should not crash.""" + try: + result = definition_to_type("struct test { int count; int data[]; };") + self.assertTrue(hasattr(result, '__annotations__')) + except (ValueError, TypeError): + pass + except AttributeError: + self.fail("arr_to_type crashed with AttributeError on unsized array - should handle gracefully") + + +class ForwardTypedefTest(unittest.TestCase): + """Forward typedef references are a known parser limitation.""" + + def setUp(self): + clear_parser_cache() + + def tearDown(self): + clear_parser_cache() + + def test_chained_typedefs_in_order(self): + """Chained typedefs in declaration order must work.""" + t = definition_to_type(""" + typedef unsigned int u32; + typedef u32 mytype; + struct S { mytype x; }; + """) + data = (42).to_bytes(4, "little") + s = t.from_bytes(data) + self.assertEqual(s.x.value, 42) + + def test_forward_typedef_reference_raises(self): + """Forward typedef reference (use before define) must raise a clear error, not crash.""" + with self.assertRaises((ValueError, TypeError)): + definition_to_type(""" + typedef mytype1 mytype2; + typedef unsigned int mytype1; + struct S { mytype2 x; }; + """) + + if __name__ == "__main__": unittest.main() diff --git a/test/scripts/struct_unit_test.py b/test/scripts/struct_test.py similarity index 91% rename from test/scripts/struct_unit_test.py rename to test/scripts/struct_test.py index 3d8f704..5784d63 100644 --- a/test/scripts/struct_unit_test.py +++ b/test/scripts/struct_test.py @@ -651,5 +651,58 @@ class flags_t(struct): self.assertIn("execute", dump) +class StructAttributeCollisionTest(unittest.TestCase): + """Struct members named after internal attributes must not break core methods.""" + + def test_struct_with_frozen_field_to_bytes(self): + """A struct with a field named '_frozen' must still serialize correctly after freeze.""" + class s_t(struct): + _frozen: c_int + b: c_int + + memory = b"" + memory += (10).to_bytes(4, "little") + memory += (20).to_bytes(4, "little") + + s = s_t.from_bytes(memory) + self.assertEqual(s.to_bytes(), memory) + + def test_struct_with_frozen_field_hexdump(self): + """A struct with a '_frozen' field must still produce a hexdump.""" + class s_t(struct): + _frozen: c_int + + s = s_t.from_bytes((42).to_bytes(4, "little")) + dump = s.hexdump() + self.assertIn("2a", dump) + + def test_struct_with_members_field_eq(self): + """A struct with a field named '_members' must still support equality.""" + class s_t(struct): + _members: c_int + + a = s_t.from_bytes((1).to_bytes(4, "little")) + b = s_t.from_bytes((1).to_bytes(4, "little")) + self.assertEqual(a, b) + + def test_struct_with_members_field_to_dict(self): + """A struct with a field named '_members' must still support to_dict.""" + class s_t(struct): + _members: c_int + + s = s_t.from_bytes((5).to_bytes(4, "little")) + d = s.to_dict() + self.assertEqual(d["_members"], 5) + + def test_struct_with_frozen_struct_bytes_field(self): + """A field named '_frozen_struct_bytes' must not break freeze/to_bytes.""" + class s_t(struct): + _frozen_struct_bytes: c_int + + memory = (99).to_bytes(4, "little") + s = s_t.from_bytes(memory) + self.assertEqual(s.to_bytes(), memory) + + if __name__ == "__main__": unittest.main() diff --git a/test/scripts/tagged_union_test.py b/test/scripts/tagged_union_test.py index 86ae62c..c82b0a2 100644 --- a/test/scripts/tagged_union_test.py +++ b/test/scripts/tagged_union_test.py @@ -7,7 +7,7 @@ import struct as pystruct import unittest -from libdestruct import c_float, c_int, c_long, inflater, size_of, struct +from libdestruct import c_char, c_float, c_int, c_long, inflater, size_of, struct from libdestruct.common.union import tagged_union, union, union_of @@ -223,3 +223,65 @@ class s_t(struct): pystruct.pack_into("f", 3.14) + f = c_float.from_bytes(original, endianness="big") + self.assertAlmostEqual(f.value, 3.14, places=5) + self.assertEqual(f.to_bytes(), original) + + def test_c_double_big_endian(self): + original = pystruct.pack(">d", 2.718) + d = c_double.from_bytes(original, endianness="big") + self.assertAlmostEqual(d.value, 2.718, places=3) + self.assertEqual(d.to_bytes(), original) + + +class ComparisonOperatorSafetyTest(unittest.TestCase): + """Comparison operators must not raise TypeError for incompatible obj types.""" + + def test_lt_primitive_vs_struct_returns_not_implemented(self): + """c_int < struct should return NotImplemented, not raise TypeError.""" + class s_t(struct): + x: c_int + + memory = bytearray(4) + lib = inflater(memory) + val = lib.inflate(c_int, 0) + s = lib.inflate(s_t, 0) + + result = val.__lt__(s) + self.assertIs(result, NotImplemented) + + def test_gt_primitive_vs_struct_returns_not_implemented(self): + class s_t(struct): + x: c_int + + memory = bytearray(4) + lib = inflater(memory) + val = lib.inflate(c_int, 0) + s = lib.inflate(s_t, 0) + + result = val.__gt__(s) + self.assertIs(result, NotImplemented) + + def test_le_primitive_vs_struct_returns_not_implemented(self): + class s_t(struct): + x: c_int + + memory = bytearray(4) + lib = inflater(memory) + val = lib.inflate(c_int, 0) + s = lib.inflate(s_t, 0) + + result = val.__le__(s) + self.assertIs(result, NotImplemented) + + def test_ge_primitive_vs_struct_returns_not_implemented(self): + class s_t(struct): + x: c_int + + memory = bytearray(4) + lib = inflater(memory) + val = lib.inflate(c_int, 0) + s = lib.inflate(s_t, 0) + + result = val.__ge__(s) + self.assertIs(result, NotImplemented) + + def test_eq_primitive_vs_struct_returns_not_implemented(self): + class s_t(struct): + x: c_int + + memory = bytearray(4) + lib = inflater(memory) + val = lib.inflate(c_int, 0) + s = lib.inflate(s_t, 0) + + result = val.__eq__(s) + self.assertIs(result, NotImplemented) + + def test_ne_primitive_vs_struct_returns_not_implemented(self): + class s_t(struct): + x: c_int + + memory = bytearray(4) + lib = inflater(memory) + val = lib.inflate(c_int, 0) + s = lib.inflate(s_t, 0) + + result = val.__ne__(s) + self.assertIs(result, NotImplemented) + + def test_lt_between_compatible_primitives_works(self): + """Comparisons between compatible primitives should still work.""" + memory = bytearray(8) + lib = inflater(memory) + a = lib.inflate(c_int, 0) + b = lib.inflate(c_int, 4) + a.value = 1 + b.value = 2 + + self.assertTrue(a < b) + self.assertFalse(b < a) + + def test_comparison_with_raw_int(self): + memory = bytearray(4) + lib = inflater(memory) + a = lib.inflate(c_int, 0) + a.value = 5 + + self.assertTrue(a < 10) + self.assertTrue(a > 2) + self.assertTrue(a <= 5) + self.assertTrue(a >= 5) + + +class PtrUnwrapLengthZeroTest(unittest.TestCase): + """ptr.unwrap(0) must read 0 bytes, not 1.""" + + def test_unwrap_length_zero_returns_empty(self): + """unwrap(0) should return 0 bytes, not 1 byte.""" + memory = bytearray(16) + memory[0:8] = (8).to_bytes(8, "little") + memory[8] = 0xAB + + p = ptr(MemoryResolver(memory, 0)) + + result = p.unwrap(0) + self.assertEqual(len(result), 0) + self.assertEqual(result, b"") + + def test_unwrap_length_none_returns_one_byte(self): + """unwrap() (default None) should still return 1 byte.""" + memory = bytearray(16) + memory[0:8] = (8).to_bytes(8, "little") + memory[8] = 0xAB + + p = ptr(MemoryResolver(memory, 0)) + + result = p.unwrap() + self.assertEqual(len(result), 1) + self.assertEqual(result, bytes([0xAB])) + + def test_try_unwrap_length_zero(self): + """try_unwrap(0) should also return empty bytes, not 1 byte.""" + memory = bytearray(16) + memory[0:8] = (8).to_bytes(8, "little") + memory[8] = 0xAB + + p = ptr(MemoryResolver(memory, 0)) + + result = p.try_unwrap(0) + self.assertIsNotNone(result) + self.assertEqual(len(result), 0) + + +class TypeRegistryDeduplicationTest(unittest.TestCase): + """Repeated handler registration must not accumulate duplicates.""" + + def test_generic_handler_not_duplicated(self): + """Registering the same handler twice must not produce duplicate entries.""" + registry = TypeRegistry() + + class DummyType: + pass + + def dummy_handler(item, args, owner): + return None + + initial_count = len(registry.generic_handlers.get(DummyType, [])) + + registry.register_generic_handler(DummyType, dummy_handler) + registry.register_generic_handler(DummyType, dummy_handler) + + count = len(registry.generic_handlers[DummyType]) + self.assertEqual(count, initial_count + 1) + + def test_instance_handler_not_duplicated(self): + """Registering the same instance handler twice must not produce duplicate entries.""" + registry = TypeRegistry() + + class DummyField: + pass + + def dummy_handler(item, annotation, owner): + return None + + initial_count = len(registry.instance_handlers.get(DummyField, [])) + + registry.register_instance_handler(DummyField, dummy_handler) + registry.register_instance_handler(DummyField, dummy_handler) + + count = len(registry.instance_handlers[DummyField]) + self.assertEqual(count, initial_count + 1) + + def test_type_handler_not_duplicated(self): + """Registering the same type handler twice must not produce duplicate entries.""" + registry = TypeRegistry() + + class DummyParent: + pass + + def dummy_handler(item): + return None + + initial_count = len(registry.type_handlers.get(DummyParent, [])) + + registry.register_type_handler(DummyParent, dummy_handler) + registry.register_type_handler(DummyParent, dummy_handler) + + count = len(registry.type_handlers[DummyParent]) + self.assertEqual(count, initial_count + 1) + + if __name__ == "__main__": unittest.main()