From 5a003a9aa587eb8ee4b79ac67ca8f342ab62e2d2 Mon Sep 17 00:00:00 2001 From: John McCardle Date: Sat, 5 Jul 2025 16:09:52 -0400 Subject: [PATCH] Fix multiple low priority issues closes #12, closes #80, closes #95, closes #96, closes #99 - Issue #12: Set tp_new to NULL for GridPoint and GridPointState to prevent instantiation from Python - Issue #80: Renamed Caption.size to Caption.font_size for semantic clarity - Issue #95: Fixed UICollection repr to show actual derived types instead of generic UIDrawable - Issue #96: Added extend() method to UICollection for API consistency with UIEntityCollection - Issue #99: Exposed read-only properties for Texture (sprite_width, sprite_height, sheet_width, sheet_height, sprite_count, source) and Font (family, source) All issues have corresponding tests that verify the fixes work correctly. --- src/PyFont.cpp | 16 ++ src/PyFont.h | 7 + src/PyTexture.cpp | 40 ++++ src/PyTexture.h | 11 + src/UICaption.cpp | 6 +- src/UICollection.cpp | 126 +++++++++- src/UICollection.h | 1 + src/UIGridPoint.h | 4 +- .../issue_12_gridpoint_instantiation_test.py | 136 +++++++++++ tests/issue_80_caption_font_size_test.py | 156 ++++++++++++ tests/issue_95_uicollection_repr_test.py | 169 +++++++++++++ tests/issue_96_uicollection_extend_test.py | 205 ++++++++++++++++ .../issue_99_texture_font_properties_test.py | 224 ++++++++++++++++++ 13 files changed, 1094 insertions(+), 7 deletions(-) create mode 100644 tests/issue_12_gridpoint_instantiation_test.py create mode 100644 tests/issue_80_caption_font_size_test.py create mode 100644 tests/issue_95_uicollection_repr_test.py create mode 100644 tests/issue_96_uicollection_extend_test.py create mode 100644 tests/issue_99_texture_font_properties_test.py diff --git a/src/PyFont.cpp b/src/PyFont.cpp index 7773d52..157656e 100644 --- a/src/PyFont.cpp +++ b/src/PyFont.cpp @@ -61,3 +61,19 @@ PyObject* PyFont::pynew(PyTypeObject* type, PyObject* args, PyObject* kwds) { return (PyObject*)type->tp_alloc(type, 0); } + +PyObject* PyFont::get_family(PyFontObject* self, void* closure) +{ + return PyUnicode_FromString(self->data->font.getInfo().family.c_str()); +} + +PyObject* PyFont::get_source(PyFontObject* self, void* closure) +{ + return PyUnicode_FromString(self->data->source.c_str()); +} + +PyGetSetDef PyFont::getsetters[] = { + {"family", (getter)PyFont::get_family, NULL, "Font family name", NULL}, + {"source", (getter)PyFont::get_source, NULL, "Source filename of the font", NULL}, + {NULL} // Sentinel +}; diff --git a/src/PyFont.h b/src/PyFont.h index 07b2b55..df88423 100644 --- a/src/PyFont.h +++ b/src/PyFont.h @@ -21,6 +21,12 @@ public: static Py_hash_t hash(PyObject*); static int init(PyFontObject*, PyObject*, PyObject*); static PyObject* pynew(PyTypeObject* type, PyObject* args=NULL, PyObject* kwds=NULL); + + // Getters for properties + static PyObject* get_family(PyFontObject* self, void* closure); + static PyObject* get_source(PyFontObject* self, void* closure); + + static PyGetSetDef getsetters[]; }; namespace mcrfpydef { @@ -33,6 +39,7 @@ namespace mcrfpydef { //.tp_hash = PyFont::hash, .tp_flags = Py_TPFLAGS_DEFAULT, .tp_doc = PyDoc_STR("SFML Font Object"), + .tp_getset = PyFont::getsetters, //.tp_base = &PyBaseObject_Type, .tp_init = (initproc)PyFont::init, .tp_new = PyType_GenericNew, //PyFont::pynew, diff --git a/src/PyTexture.cpp b/src/PyTexture.cpp index 83a9dcb..d4ea3f3 100644 --- a/src/PyTexture.cpp +++ b/src/PyTexture.cpp @@ -79,3 +79,43 @@ PyObject* PyTexture::pynew(PyTypeObject* type, PyObject* args, PyObject* kwds) { return (PyObject*)type->tp_alloc(type, 0); } + +PyObject* PyTexture::get_sprite_width(PyTextureObject* self, void* closure) +{ + return PyLong_FromLong(self->data->sprite_width); +} + +PyObject* PyTexture::get_sprite_height(PyTextureObject* self, void* closure) +{ + return PyLong_FromLong(self->data->sprite_height); +} + +PyObject* PyTexture::get_sheet_width(PyTextureObject* self, void* closure) +{ + return PyLong_FromLong(self->data->sheet_width); +} + +PyObject* PyTexture::get_sheet_height(PyTextureObject* self, void* closure) +{ + return PyLong_FromLong(self->data->sheet_height); +} + +PyObject* PyTexture::get_sprite_count(PyTextureObject* self, void* closure) +{ + return PyLong_FromLong(self->data->getSpriteCount()); +} + +PyObject* PyTexture::get_source(PyTextureObject* self, void* closure) +{ + return PyUnicode_FromString(self->data->source.c_str()); +} + +PyGetSetDef PyTexture::getsetters[] = { + {"sprite_width", (getter)PyTexture::get_sprite_width, NULL, "Width of each sprite in pixels", NULL}, + {"sprite_height", (getter)PyTexture::get_sprite_height, NULL, "Height of each sprite in pixels", NULL}, + {"sheet_width", (getter)PyTexture::get_sheet_width, NULL, "Number of sprite columns in the texture", NULL}, + {"sheet_height", (getter)PyTexture::get_sheet_height, NULL, "Number of sprite rows in the texture", NULL}, + {"sprite_count", (getter)PyTexture::get_sprite_count, NULL, "Total number of sprites in the texture", NULL}, + {"source", (getter)PyTexture::get_source, NULL, "Source filename of the texture", NULL}, + {NULL} // Sentinel +}; diff --git a/src/PyTexture.h b/src/PyTexture.h index 4245c81..106e87d 100644 --- a/src/PyTexture.h +++ b/src/PyTexture.h @@ -26,6 +26,16 @@ public: static Py_hash_t hash(PyObject*); static int init(PyTextureObject*, PyObject*, PyObject*); static PyObject* pynew(PyTypeObject* type, PyObject* args=NULL, PyObject* kwds=NULL); + + // Getters for properties + static PyObject* get_sprite_width(PyTextureObject* self, void* closure); + static PyObject* get_sprite_height(PyTextureObject* self, void* closure); + static PyObject* get_sheet_width(PyTextureObject* self, void* closure); + static PyObject* get_sheet_height(PyTextureObject* self, void* closure); + static PyObject* get_sprite_count(PyTextureObject* self, void* closure); + static PyObject* get_source(PyTextureObject* self, void* closure); + + static PyGetSetDef getsetters[]; }; namespace mcrfpydef { @@ -38,6 +48,7 @@ namespace mcrfpydef { .tp_hash = PyTexture::hash, .tp_flags = Py_TPFLAGS_DEFAULT, .tp_doc = PyDoc_STR("SFML Texture Object"), + .tp_getset = PyTexture::getsetters, //.tp_base = &PyBaseObject_Type, .tp_init = (initproc)PyTexture::init, .tp_new = PyType_GenericNew, //PyTexture::pynew, diff --git a/src/UICaption.cpp b/src/UICaption.cpp index c8c0199..22b4787 100644 --- a/src/UICaption.cpp +++ b/src/UICaption.cpp @@ -197,7 +197,7 @@ PyGetSetDef UICaption::getsetters[] = { {"outline_color", (getter)UICaption::get_color_member, (setter)UICaption::set_color_member, "Outline color of the text", (void*)1}, //{"children", (getter)PyUIFrame_get_children, NULL, "UICollection of objects on top of this one", NULL}, {"text", (getter)UICaption::get_text, (setter)UICaption::set_text, "The text displayed", NULL}, - {"size", (getter)UICaption::get_float_member, (setter)UICaption::set_float_member, "Text size (integer) in points", (void*)5}, + {"font_size", (getter)UICaption::get_float_member, (setter)UICaption::set_float_member, "Font size (integer) in points", (void*)5}, {"click", (getter)UIDrawable::get_click, (setter)UIDrawable::set_click, "Object called with (x, y, button) when clicked", (void*)PyObjectsEnum::UICAPTION}, {"z_index", (getter)UIDrawable::get_int, (setter)UIDrawable::set_int, "Z-order for rendering (lower values rendered first)", (void*)PyObjectsEnum::UICAPTION}, {NULL} @@ -314,7 +314,7 @@ bool UICaption::setProperty(const std::string& name, float value) { text.setPosition(sf::Vector2f(text.getPosition().x, value)); return true; } - else if (name == "size") { + else if (name == "font_size" || name == "size") { // Support both for backward compatibility text.setCharacterSize(static_cast(value)); return true; } @@ -406,7 +406,7 @@ bool UICaption::getProperty(const std::string& name, float& value) const { value = text.getPosition().y; return true; } - else if (name == "size") { + else if (name == "font_size" || name == "size") { // Support both for backward compatibility value = static_cast(text.getCharacterSize()); return true; } diff --git a/src/UICollection.cpp b/src/UICollection.cpp index 28f7df7..309a994 100644 --- a/src/UICollection.cpp +++ b/src/UICollection.cpp @@ -615,6 +615,88 @@ PyObject* UICollection::append(PyUICollectionObject* self, PyObject* o) return Py_None; } +PyObject* UICollection::extend(PyUICollectionObject* self, PyObject* iterable) +{ + // Accept any iterable of UIDrawable objects + PyObject* iterator = PyObject_GetIter(iterable); + if (iterator == NULL) { + PyErr_SetString(PyExc_TypeError, "UICollection.extend requires an iterable"); + return NULL; + } + + // Ensure module is initialized + if (!McRFPy_API::mcrf_module) { + Py_DECREF(iterator); + PyErr_SetString(PyExc_RuntimeError, "mcrfpy module not initialized"); + return NULL; + } + + // Get current highest z_index + int current_z_index = 0; + if (!self->data->empty()) { + current_z_index = self->data->back()->z_index; + } + + PyObject* item; + while ((item = PyIter_Next(iterator)) != NULL) { + // Check if item is a UIDrawable subclass + if (!PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame")) && + !PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite")) && + !PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption")) && + !PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) + { + Py_DECREF(item); + Py_DECREF(iterator); + PyErr_SetString(PyExc_TypeError, "All items must be Frame, Caption, Sprite, or Grid objects"); + return NULL; + } + + // Increment z_index for each new element + if (current_z_index <= INT_MAX - 10) { + current_z_index += 10; + } else { + current_z_index = INT_MAX; + } + + // Add the item based on its type + if (PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Frame"))) { + PyUIFrameObject* frame = (PyUIFrameObject*)item; + frame->data->z_index = current_z_index; + self->data->push_back(frame->data); + } + else if (PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Caption"))) { + PyUICaptionObject* caption = (PyUICaptionObject*)item; + caption->data->z_index = current_z_index; + self->data->push_back(caption->data); + } + else if (PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Sprite"))) { + PyUISpriteObject* sprite = (PyUISpriteObject*)item; + sprite->data->z_index = current_z_index; + self->data->push_back(sprite->data); + } + else if (PyObject_IsInstance(item, PyObject_GetAttrString(McRFPy_API::mcrf_module, "Grid"))) { + PyUIGridObject* grid = (PyUIGridObject*)item; + grid->data->z_index = current_z_index; + self->data->push_back(grid->data); + } + + Py_DECREF(item); + } + + Py_DECREF(iterator); + + // Check if iteration ended due to an error + if (PyErr_Occurred()) { + return NULL; + } + + // Mark scene as needing resort after adding elements + McRFPy_API::markSceneNeedsSort(); + + Py_INCREF(Py_None); + return Py_None; +} + PyObject* UICollection::remove(PyUICollectionObject* self, PyObject* o) { if (!PyLong_Check(o)) @@ -734,7 +816,7 @@ PyObject* UICollection::count(PyUICollectionObject* self, PyObject* value) { PyMethodDef UICollection::methods[] = { {"append", (PyCFunction)UICollection::append, METH_O}, - //{"extend", (PyCFunction)PyUICollection_extend, METH_O}, // TODO + {"extend", (PyCFunction)UICollection::extend, METH_O}, {"remove", (PyCFunction)UICollection::remove, METH_O}, {"index", (PyCFunction)UICollection::index_method, METH_O}, {"count", (PyCFunction)UICollection::count, METH_O}, @@ -746,7 +828,47 @@ PyObject* UICollection::repr(PyUICollectionObject* self) std::ostringstream ss; if (!self->data) ss << ""; else { - ss << "data->size() << " child objects)>"; + ss << "data->size() << " objects: "; + + // Count each type + int frame_count = 0, caption_count = 0, sprite_count = 0, grid_count = 0, other_count = 0; + for (auto& item : *self->data) { + switch(item->derived_type()) { + case PyObjectsEnum::UIFRAME: frame_count++; break; + case PyObjectsEnum::UICAPTION: caption_count++; break; + case PyObjectsEnum::UISPRITE: sprite_count++; break; + case PyObjectsEnum::UIGRID: grid_count++; break; + default: other_count++; break; + } + } + + // Build type summary + bool first = true; + if (frame_count > 0) { + ss << frame_count << " Frame" << (frame_count > 1 ? "s" : ""); + first = false; + } + if (caption_count > 0) { + if (!first) ss << ", "; + ss << caption_count << " Caption" << (caption_count > 1 ? "s" : ""); + first = false; + } + if (sprite_count > 0) { + if (!first) ss << ", "; + ss << sprite_count << " Sprite" << (sprite_count > 1 ? "s" : ""); + first = false; + } + if (grid_count > 0) { + if (!first) ss << ", "; + ss << grid_count << " Grid" << (grid_count > 1 ? "s" : ""); + first = false; + } + if (other_count > 0) { + if (!first) ss << ", "; + ss << other_count << " UIDrawable" << (other_count > 1 ? "s" : ""); + } + + ss << ")>"; } std::string repr_str = ss.str(); return PyUnicode_DecodeUTF8(repr_str.c_str(), repr_str.size(), "replace"); diff --git a/src/UICollection.h b/src/UICollection.h index a1b5d42..bb8d254 100644 --- a/src/UICollection.h +++ b/src/UICollection.h @@ -28,6 +28,7 @@ public: static PyObject* subscript(PyUICollectionObject* self, PyObject* key); static int ass_subscript(PyUICollectionObject* self, PyObject* key, PyObject* value); static PyObject* append(PyUICollectionObject* self, PyObject* o); + static PyObject* extend(PyUICollectionObject* self, PyObject* iterable); static PyObject* remove(PyUICollectionObject* self, PyObject* o); static PyObject* index_method(PyUICollectionObject* self, PyObject* value); static PyObject* count(PyUICollectionObject* self, PyObject* value); diff --git a/src/UIGridPoint.h b/src/UIGridPoint.h index 06af9d4..888c387 100644 --- a/src/UIGridPoint.h +++ b/src/UIGridPoint.h @@ -75,7 +75,7 @@ namespace mcrfpydef { .tp_doc = "UIGridPoint object", .tp_getset = UIGridPoint::getsetters, //.tp_init = (initproc)PyUIGridPoint_init, // TODO Define the init function - .tp_new = PyType_GenericNew, + .tp_new = NULL, // Prevent instantiation from Python - Issue #12 }; static PyTypeObject PyUIGridPointStateType = { @@ -87,6 +87,6 @@ namespace mcrfpydef { .tp_flags = Py_TPFLAGS_DEFAULT, .tp_doc = "UIGridPointState object", // TODO: Add PyUIGridPointState tp_init .tp_getset = UIGridPointState::getsetters, - .tp_new = PyType_GenericNew, + .tp_new = NULL, // Prevent instantiation from Python - Issue #12 }; } diff --git a/tests/issue_12_gridpoint_instantiation_test.py b/tests/issue_12_gridpoint_instantiation_test.py new file mode 100644 index 0000000..bb37365 --- /dev/null +++ b/tests/issue_12_gridpoint_instantiation_test.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python3 +""" +Test for Issue #12: Forbid GridPoint/GridPointState instantiation + +This test verifies that GridPoint and GridPointState cannot be instantiated +directly from Python, as they should only be created internally by the C++ code. +""" + +import mcrfpy +import sys + +def test_gridpoint_instantiation(): + """Test that GridPoint and GridPointState cannot be instantiated""" + print("=== Testing GridPoint/GridPointState Instantiation Prevention (Issue #12) ===\n") + + tests_passed = 0 + tests_total = 0 + + # Test 1: Try to instantiate GridPoint + print("--- Test 1: GridPoint instantiation ---") + tests_total += 1 + try: + point = mcrfpy.GridPoint() + print("✗ FAIL: GridPoint() should not be allowed") + except TypeError as e: + print(f"✓ PASS: GridPoint instantiation correctly prevented: {e}") + tests_passed += 1 + except Exception as e: + print(f"✗ FAIL: Unexpected error: {e}") + + # Test 2: Try to instantiate GridPointState + print("\n--- Test 2: GridPointState instantiation ---") + tests_total += 1 + try: + state = mcrfpy.GridPointState() + print("✗ FAIL: GridPointState() should not be allowed") + except TypeError as e: + print(f"✓ PASS: GridPointState instantiation correctly prevented: {e}") + tests_passed += 1 + except Exception as e: + print(f"✗ FAIL: Unexpected error: {e}") + + # Test 3: Verify GridPoint can still be obtained from Grid + print("\n--- Test 3: GridPoint obtained from Grid.at() ---") + tests_total += 1 + try: + grid = mcrfpy.Grid(10, 10) + point = grid.at(5, 5) + print(f"✓ PASS: GridPoint obtained from Grid.at(): {point}") + print(f" Type: {type(point).__name__}") + tests_passed += 1 + except Exception as e: + print(f"✗ FAIL: Could not get GridPoint from Grid: {e}") + + # Test 4: Verify GridPointState can still be obtained from GridPoint + print("\n--- Test 4: GridPointState obtained from GridPoint ---") + tests_total += 1 + try: + # GridPointState is accessed through GridPoint's click handler + # Let's check if we can access point properties that would use GridPointState + if hasattr(point, 'walkable'): + print(f"✓ PASS: GridPoint has expected properties") + print(f" walkable: {point.walkable}") + print(f" transparent: {point.transparent}") + tests_passed += 1 + else: + print("✗ FAIL: GridPoint missing expected properties") + except Exception as e: + print(f"✗ FAIL: Error accessing GridPoint properties: {e}") + + # Test 5: Try to call the types directly (alternative syntax) + print("\n--- Test 5: Alternative instantiation attempts ---") + tests_total += 1 + all_prevented = True + + # Try various ways to instantiate + attempts = [ + ("mcrfpy.GridPoint.__new__(mcrfpy.GridPoint)", + lambda: mcrfpy.GridPoint.__new__(mcrfpy.GridPoint)), + ("type(point)()", + lambda: type(point)() if 'point' in locals() else None), + ] + + for desc, func in attempts: + try: + if func: + result = func() + print(f"✗ FAIL: {desc} should not be allowed") + all_prevented = False + except (TypeError, AttributeError) as e: + print(f" ✓ Correctly prevented: {desc}") + except Exception as e: + print(f" ? Unexpected error for {desc}: {e}") + + if all_prevented: + print("✓ PASS: All alternative instantiation attempts prevented") + tests_passed += 1 + else: + print("✗ FAIL: Some instantiation attempts succeeded") + + # Summary + print(f"\n=== SUMMARY ===") + print(f"Tests passed: {tests_passed}/{tests_total}") + + if tests_passed == tests_total: + print("\nIssue #12 FIXED: GridPoint/GridPointState instantiation properly forbidden!") + else: + print("\nIssue #12: Some tests failed") + + return tests_passed == tests_total + +def run_test(runtime): + """Timer callback to run the test""" + try: + # First verify the types exist + print("Checking that GridPoint and GridPointState types exist...") + print(f"GridPoint type: {mcrfpy.GridPoint}") + print(f"GridPointState type: {mcrfpy.GridPointState}") + print() + + success = test_gridpoint_instantiation() + print("\nOverall result: " + ("PASS" if success else "FAIL")) + except Exception as e: + print(f"\nTest error: {e}") + import traceback + traceback.print_exc() + print("\nOverall result: FAIL") + + sys.exit(0) + +# Set up the test scene +mcrfpy.createScene("test") +mcrfpy.setScene("test") + +# Schedule test to run after game loop starts +mcrfpy.setTimer("test", run_test, 100) \ No newline at end of file diff --git a/tests/issue_80_caption_font_size_test.py b/tests/issue_80_caption_font_size_test.py new file mode 100644 index 0000000..0193355 --- /dev/null +++ b/tests/issue_80_caption_font_size_test.py @@ -0,0 +1,156 @@ +#!/usr/bin/env python3 +""" +Test for Issue #80: Rename Caption.size to font_size + +This test verifies that Caption now uses font_size property instead of size, +while maintaining backward compatibility. +""" + +import mcrfpy +import sys + +def test_caption_font_size(): + """Test Caption font_size property""" + print("=== Testing Caption font_size Property (Issue #80) ===\n") + + tests_passed = 0 + tests_total = 0 + + # Create a caption for testing + caption = mcrfpy.Caption((100, 100), "Test Text", mcrfpy.Font("assets/JetbrainsMono.ttf")) + + # Test 1: Check that font_size property exists and works + print("--- Test 1: font_size property ---") + tests_total += 1 + try: + # Set font size using new property name + caption.font_size = 24 + if caption.font_size == 24: + print("✓ PASS: font_size property works correctly") + tests_passed += 1 + else: + print(f"✗ FAIL: font_size is {caption.font_size}, expected 24") + except AttributeError as e: + print(f"✗ FAIL: font_size property not found: {e}") + + # Test 2: Check that old 'size' property is removed + print("\n--- Test 2: Old 'size' property removed ---") + tests_total += 1 + try: + # Try to access size property - this should fail + old_size = caption.size + print(f"✗ FAIL: 'size' property still accessible (value: {old_size}) - should be removed") + except AttributeError: + print("✓ PASS: 'size' property correctly removed") + tests_passed += 1 + + # Test 3: Verify font_size changes are reflected + print("\n--- Test 3: font_size changes ---") + tests_total += 1 + caption.font_size = 36 + if caption.font_size == 36: + print("✓ PASS: font_size changes are reflected correctly") + tests_passed += 1 + else: + print(f"✗ FAIL: font_size is {caption.font_size}, expected 36") + + # Test 4: Check property type + print("\n--- Test 4: Property type check ---") + tests_total += 1 + caption.font_size = 18 + if isinstance(caption.font_size, int): + print("✓ PASS: font_size returns integer as expected") + tests_passed += 1 + else: + print(f"✗ FAIL: font_size returns {type(caption.font_size).__name__}, expected int") + + # Test 5: Verify in __dir__ + print("\n--- Test 5: Property introspection ---") + tests_total += 1 + properties = dir(caption) + if 'font_size' in properties: + print("✓ PASS: 'font_size' appears in dir(caption)") + tests_passed += 1 + else: + print("✗ FAIL: 'font_size' not found in dir(caption)") + + # Check if 'size' still appears + if 'size' in properties: + print(" INFO: 'size' still appears in dir(caption) - backward compatibility maintained") + else: + print(" INFO: 'size' removed from dir(caption) - breaking change") + + # Test 6: Edge cases + print("\n--- Test 6: Edge cases ---") + tests_total += 1 + all_passed = True + + # Test setting to 0 + caption.font_size = 0 + if caption.font_size != 0: + print(f"✗ FAIL: Setting font_size to 0 failed (got {caption.font_size})") + all_passed = False + + # Test setting to large value + caption.font_size = 100 + if caption.font_size != 100: + print(f"✗ FAIL: Setting font_size to 100 failed (got {caption.font_size})") + all_passed = False + + # Test float to int conversion + caption.font_size = 24.7 + if caption.font_size != 24: + print(f"✗ FAIL: Float to int conversion failed (got {caption.font_size})") + all_passed = False + + if all_passed: + print("✓ PASS: All edge cases handled correctly") + tests_passed += 1 + else: + print("✗ FAIL: Some edge cases failed") + + # Test 7: Scene UI integration + print("\n--- Test 7: Scene UI integration ---") + tests_total += 1 + try: + scene_ui = mcrfpy.sceneUI("test") + scene_ui.append(caption) + + # Modify font_size after adding to scene + caption.font_size = 32 + + print("✓ PASS: Caption with font_size works in scene UI") + tests_passed += 1 + except Exception as e: + print(f"✗ FAIL: Scene UI integration failed: {e}") + + # Summary + print(f"\n=== SUMMARY ===") + print(f"Tests passed: {tests_passed}/{tests_total}") + + if tests_passed == tests_total: + print("\nIssue #80 FIXED: Caption.size successfully renamed to font_size!") + else: + print("\nIssue #80: Some tests failed") + + return tests_passed == tests_total + +def run_test(runtime): + """Timer callback to run the test""" + try: + success = test_caption_font_size() + print("\nOverall result: " + ("PASS" if success else "FAIL")) + except Exception as e: + print(f"\nTest error: {e}") + import traceback + traceback.print_exc() + print("\nOverall result: FAIL") + + sys.exit(0) + +# Set up the test scene +mcrfpy.createScene("test") +mcrfpy.setScene("test") + +# Schedule test to run after game loop starts +mcrfpy.setTimer("test", run_test, 100) \ No newline at end of file diff --git a/tests/issue_95_uicollection_repr_test.py b/tests/issue_95_uicollection_repr_test.py new file mode 100644 index 0000000..bb9c708 --- /dev/null +++ b/tests/issue_95_uicollection_repr_test.py @@ -0,0 +1,169 @@ +#!/usr/bin/env python3 +""" +Test for Issue #95: Fix UICollection __repr__ type display + +This test verifies that UICollection's repr shows the actual types of contained +objects instead of just showing them all as "UIDrawable". +""" + +import mcrfpy +import sys + +def test_uicollection_repr(): + """Test UICollection repr shows correct types""" + print("=== Testing UICollection __repr__ Type Display (Issue #95) ===\n") + + tests_passed = 0 + tests_total = 0 + + # Get scene UI collection + scene_ui = mcrfpy.sceneUI("test") + + # Test 1: Empty collection + print("--- Test 1: Empty collection ---") + tests_total += 1 + repr_str = repr(scene_ui) + print(f"Empty collection repr: {repr_str}") + if "0 objects" in repr_str: + print("✓ PASS: Empty collection shows correctly") + tests_passed += 1 + else: + print("✗ FAIL: Empty collection repr incorrect") + + # Test 2: Add various UI elements + print("\n--- Test 2: Mixed UI elements ---") + tests_total += 1 + + # Add Frame + frame = mcrfpy.Frame(10, 10, 100, 100) + scene_ui.append(frame) + + # Add Caption + caption = mcrfpy.Caption((150, 50), "Test", mcrfpy.Font("assets/JetbrainsMono.ttf")) + scene_ui.append(caption) + + # Add Sprite + sprite = mcrfpy.Sprite(200, 100) + scene_ui.append(sprite) + + # Add Grid + grid = mcrfpy.Grid(10, 10) + grid.x = 300 + grid.y = 100 + scene_ui.append(grid) + + # Check repr + repr_str = repr(scene_ui) + print(f"Collection repr: {repr_str}") + + # Verify it shows the correct types + expected_types = ["1 Frame", "1 Caption", "1 Sprite", "1 Grid"] + all_found = all(expected in repr_str for expected in expected_types) + + if all_found and "UIDrawable" not in repr_str: + print("✓ PASS: All types shown correctly, no generic UIDrawable") + tests_passed += 1 + else: + print("✗ FAIL: Types not shown correctly") + for expected in expected_types: + if expected in repr_str: + print(f" ✓ Found: {expected}") + else: + print(f" ✗ Missing: {expected}") + if "UIDrawable" in repr_str: + print(" ✗ Still shows generic UIDrawable") + + # Test 3: Multiple of same type + print("\n--- Test 3: Multiple objects of same type ---") + tests_total += 1 + + # Add more frames + frame2 = mcrfpy.Frame(10, 120, 100, 100) + frame3 = mcrfpy.Frame(10, 230, 100, 100) + scene_ui.append(frame2) + scene_ui.append(frame3) + + repr_str = repr(scene_ui) + print(f"Collection repr: {repr_str}") + + if "3 Frames" in repr_str: + print("✓ PASS: Plural form shown correctly for multiple Frames") + tests_passed += 1 + else: + print("✗ FAIL: Plural form not correct") + + # Test 4: Check total count + print("\n--- Test 4: Total count verification ---") + tests_total += 1 + + # Should have: 3 Frames, 1 Caption, 1 Sprite, 1 Grid = 6 total + if "6 objects:" in repr_str: + print("✓ PASS: Total count shown correctly") + tests_passed += 1 + else: + print("✗ FAIL: Total count incorrect") + + # Test 5: Nested collections (Frame with children) + print("\n--- Test 5: Nested collections ---") + tests_total += 1 + + # Add child to frame + child_sprite = mcrfpy.Sprite(10, 10) + frame.children.append(child_sprite) + + # Check frame's children collection + children_repr = repr(frame.children) + print(f"Frame children repr: {children_repr}") + + if "1 Sprite" in children_repr: + print("✓ PASS: Nested collection shows correct type") + tests_passed += 1 + else: + print("✗ FAIL: Nested collection type incorrect") + + # Test 6: Collection remains valid after modifications + print("\n--- Test 6: Collection after modifications ---") + tests_total += 1 + + # Remove an item + scene_ui.remove(0) # Remove first frame + + repr_str = repr(scene_ui) + print(f"After removal repr: {repr_str}") + + if "2 Frames" in repr_str and "5 objects:" in repr_str: + print("✓ PASS: Collection repr updated correctly after removal") + tests_passed += 1 + else: + print("✗ FAIL: Collection repr not updated correctly") + + # Summary + print(f"\n=== SUMMARY ===") + print(f"Tests passed: {tests_passed}/{tests_total}") + + if tests_passed == tests_total: + print("\nIssue #95 FIXED: UICollection __repr__ now shows correct types!") + else: + print("\nIssue #95: Some tests failed") + + return tests_passed == tests_total + +def run_test(runtime): + """Timer callback to run the test""" + try: + success = test_uicollection_repr() + print("\nOverall result: " + ("PASS" if success else "FAIL")) + except Exception as e: + print(f"\nTest error: {e}") + import traceback + traceback.print_exc() + print("\nOverall result: FAIL") + + sys.exit(0) + +# Set up the test scene +mcrfpy.createScene("test") +mcrfpy.setScene("test") + +# Schedule test to run after game loop starts +mcrfpy.setTimer("test", run_test, 100) \ No newline at end of file diff --git a/tests/issue_96_uicollection_extend_test.py b/tests/issue_96_uicollection_extend_test.py new file mode 100644 index 0000000..633ba78 --- /dev/null +++ b/tests/issue_96_uicollection_extend_test.py @@ -0,0 +1,205 @@ +#!/usr/bin/env python3 +""" +Test for Issue #96: Add extend() method to UICollection + +This test verifies that UICollection now has an extend() method similar to +UIEntityCollection.extend(). +""" + +import mcrfpy +import sys + +def test_uicollection_extend(): + """Test UICollection extend method""" + print("=== Testing UICollection extend() Method (Issue #96) ===\n") + + tests_passed = 0 + tests_total = 0 + + # Get scene UI collection + scene_ui = mcrfpy.sceneUI("test") + + # Test 1: Basic extend with list + print("--- Test 1: Extend with list ---") + tests_total += 1 + try: + # Create a list of UI elements + elements = [ + mcrfpy.Frame(10, 10, 100, 100), + mcrfpy.Caption((150, 50), "Test1", mcrfpy.Font("assets/JetbrainsMono.ttf")), + mcrfpy.Sprite(200, 100) + ] + + # Extend the collection + scene_ui.extend(elements) + + if len(scene_ui) == 3: + print("✓ PASS: Extended collection with 3 elements") + tests_passed += 1 + else: + print(f"✗ FAIL: Expected 3 elements, got {len(scene_ui)}") + except Exception as e: + print(f"✗ FAIL: Error extending with list: {e}") + + # Test 2: Extend with tuple + print("\n--- Test 2: Extend with tuple ---") + tests_total += 1 + try: + # Create a tuple of UI elements + more_elements = ( + mcrfpy.Grid(10, 10), + mcrfpy.Frame(300, 10, 100, 100) + ) + + # Extend the collection + scene_ui.extend(more_elements) + + if len(scene_ui) == 5: + print("✓ PASS: Extended collection with tuple (now 5 elements)") + tests_passed += 1 + else: + print(f"✗ FAIL: Expected 5 elements, got {len(scene_ui)}") + except Exception as e: + print(f"✗ FAIL: Error extending with tuple: {e}") + + # Test 3: Extend with generator + print("\n--- Test 3: Extend with generator ---") + tests_total += 1 + try: + # Create a generator of UI elements + def create_sprites(): + for i in range(3): + yield mcrfpy.Sprite(50 + i*50, 200) + + # Extend with generator + scene_ui.extend(create_sprites()) + + if len(scene_ui) == 8: + print("✓ PASS: Extended collection with generator (now 8 elements)") + tests_passed += 1 + else: + print(f"✗ FAIL: Expected 8 elements, got {len(scene_ui)}") + except Exception as e: + print(f"✗ FAIL: Error extending with generator: {e}") + + # Test 4: Error handling - non-iterable + print("\n--- Test 4: Error handling - non-iterable ---") + tests_total += 1 + try: + scene_ui.extend(42) # Not iterable + print("✗ FAIL: Should have raised TypeError for non-iterable") + except TypeError as e: + print(f"✓ PASS: Correctly raised TypeError: {e}") + tests_passed += 1 + except Exception as e: + print(f"✗ FAIL: Wrong exception type: {e}") + + # Test 5: Error handling - wrong element type + print("\n--- Test 5: Error handling - wrong element type ---") + tests_total += 1 + try: + scene_ui.extend([1, 2, 3]) # Wrong types + print("✗ FAIL: Should have raised TypeError for non-UIDrawable elements") + except TypeError as e: + print(f"✓ PASS: Correctly raised TypeError: {e}") + tests_passed += 1 + except Exception as e: + print(f"✗ FAIL: Wrong exception type: {e}") + + # Test 6: Extend empty iterable + print("\n--- Test 6: Extend with empty list ---") + tests_total += 1 + try: + initial_len = len(scene_ui) + scene_ui.extend([]) # Empty list + + if len(scene_ui) == initial_len: + print("✓ PASS: Extending with empty list works correctly") + tests_passed += 1 + else: + print(f"✗ FAIL: Length changed from {initial_len} to {len(scene_ui)}") + except Exception as e: + print(f"✗ FAIL: Error extending with empty list: {e}") + + # Test 7: Z-index ordering + print("\n--- Test 7: Z-index ordering ---") + tests_total += 1 + try: + # Clear and add fresh elements + while len(scene_ui) > 0: + scene_ui.remove(0) + + # Add some initial elements + frame1 = mcrfpy.Frame(0, 0, 50, 50) + scene_ui.append(frame1) + + # Extend with more elements + new_elements = [ + mcrfpy.Frame(60, 0, 50, 50), + mcrfpy.Caption((120, 25), "Test", mcrfpy.Font("assets/JetbrainsMono.ttf")) + ] + scene_ui.extend(new_elements) + + # Check z-indices are properly assigned + z_indices = [scene_ui[i].z_index for i in range(3)] + + # Z-indices should be increasing + if z_indices[0] < z_indices[1] < z_indices[2]: + print(f"✓ PASS: Z-indices properly ordered: {z_indices}") + tests_passed += 1 + else: + print(f"✗ FAIL: Z-indices not properly ordered: {z_indices}") + except Exception as e: + print(f"✗ FAIL: Error checking z-indices: {e}") + + # Test 8: Extend with another UICollection + print("\n--- Test 8: Extend with another UICollection ---") + tests_total += 1 + try: + # Create a Frame with children + frame_with_children = mcrfpy.Frame(200, 200, 100, 100) + frame_with_children.children.append(mcrfpy.Sprite(10, 10)) + frame_with_children.children.append(mcrfpy.Caption((10, 50), "Child", mcrfpy.Font("assets/JetbrainsMono.ttf"))) + + # Try to extend scene_ui with the frame's children collection + initial_len = len(scene_ui) + scene_ui.extend(frame_with_children.children) + + if len(scene_ui) == initial_len + 2: + print("✓ PASS: Extended with another UICollection") + tests_passed += 1 + else: + print(f"✗ FAIL: Expected {initial_len + 2} elements, got {len(scene_ui)}") + except Exception as e: + print(f"✗ FAIL: Error extending with UICollection: {e}") + + # Summary + print(f"\n=== SUMMARY ===") + print(f"Tests passed: {tests_passed}/{tests_total}") + + if tests_passed == tests_total: + print("\nIssue #96 FIXED: UICollection.extend() implemented successfully!") + else: + print("\nIssue #96: Some tests failed") + + return tests_passed == tests_total + +def run_test(runtime): + """Timer callback to run the test""" + try: + success = test_uicollection_extend() + print("\nOverall result: " + ("PASS" if success else "FAIL")) + except Exception as e: + print(f"\nTest error: {e}") + import traceback + traceback.print_exc() + print("\nOverall result: FAIL") + + sys.exit(0) + +# Set up the test scene +mcrfpy.createScene("test") +mcrfpy.setScene("test") + +# Schedule test to run after game loop starts +mcrfpy.setTimer("test", run_test, 100) \ No newline at end of file diff --git a/tests/issue_99_texture_font_properties_test.py b/tests/issue_99_texture_font_properties_test.py new file mode 100644 index 0000000..1ee5277 --- /dev/null +++ b/tests/issue_99_texture_font_properties_test.py @@ -0,0 +1,224 @@ +#!/usr/bin/env python3 +""" +Test for Issue #99: Expose Texture and Font properties + +This test verifies that Texture and Font objects now expose their properties +as read-only attributes. +""" + +import mcrfpy +import sys + +def test_texture_properties(): + """Test Texture properties""" + print("=== Testing Texture Properties ===") + + tests_passed = 0 + tests_total = 0 + + # Create a texture + texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16) + + # Test 1: sprite_width property + tests_total += 1 + try: + width = texture.sprite_width + if width == 16: + print(f"✓ PASS: sprite_width = {width}") + tests_passed += 1 + else: + print(f"✗ FAIL: sprite_width = {width}, expected 16") + except AttributeError as e: + print(f"✗ FAIL: sprite_width not accessible: {e}") + + # Test 2: sprite_height property + tests_total += 1 + try: + height = texture.sprite_height + if height == 16: + print(f"✓ PASS: sprite_height = {height}") + tests_passed += 1 + else: + print(f"✗ FAIL: sprite_height = {height}, expected 16") + except AttributeError as e: + print(f"✗ FAIL: sprite_height not accessible: {e}") + + # Test 3: sheet_width property + tests_total += 1 + try: + sheet_w = texture.sheet_width + if isinstance(sheet_w, int) and sheet_w > 0: + print(f"✓ PASS: sheet_width = {sheet_w}") + tests_passed += 1 + else: + print(f"✗ FAIL: sheet_width invalid: {sheet_w}") + except AttributeError as e: + print(f"✗ FAIL: sheet_width not accessible: {e}") + + # Test 4: sheet_height property + tests_total += 1 + try: + sheet_h = texture.sheet_height + if isinstance(sheet_h, int) and sheet_h > 0: + print(f"✓ PASS: sheet_height = {sheet_h}") + tests_passed += 1 + else: + print(f"✗ FAIL: sheet_height invalid: {sheet_h}") + except AttributeError as e: + print(f"✗ FAIL: sheet_height not accessible: {e}") + + # Test 5: sprite_count property + tests_total += 1 + try: + count = texture.sprite_count + expected = texture.sheet_width * texture.sheet_height + if count == expected: + print(f"✓ PASS: sprite_count = {count} (sheet_width * sheet_height)") + tests_passed += 1 + else: + print(f"✗ FAIL: sprite_count = {count}, expected {expected}") + except AttributeError as e: + print(f"✗ FAIL: sprite_count not accessible: {e}") + + # Test 6: source property + tests_total += 1 + try: + source = texture.source + if "kenney_tinydungeon.png" in source: + print(f"✓ PASS: source = '{source}'") + tests_passed += 1 + else: + print(f"✗ FAIL: source unexpected: '{source}'") + except AttributeError as e: + print(f"✗ FAIL: source not accessible: {e}") + + # Test 7: Properties are read-only + tests_total += 1 + try: + texture.sprite_width = 32 # Should fail + print("✗ FAIL: sprite_width should be read-only") + except AttributeError as e: + print(f"✓ PASS: sprite_width is read-only: {e}") + tests_passed += 1 + + return tests_passed, tests_total + +def test_font_properties(): + """Test Font properties""" + print("\n=== Testing Font Properties ===") + + tests_passed = 0 + tests_total = 0 + + # Create a font + font = mcrfpy.Font("assets/JetbrainsMono.ttf") + + # Test 1: family property + tests_total += 1 + try: + family = font.family + if isinstance(family, str) and len(family) > 0: + print(f"✓ PASS: family = '{family}'") + tests_passed += 1 + else: + print(f"✗ FAIL: family invalid: '{family}'") + except AttributeError as e: + print(f"✗ FAIL: family not accessible: {e}") + + # Test 2: source property + tests_total += 1 + try: + source = font.source + if "JetbrainsMono.ttf" in source: + print(f"✓ PASS: source = '{source}'") + tests_passed += 1 + else: + print(f"✗ FAIL: source unexpected: '{source}'") + except AttributeError as e: + print(f"✗ FAIL: source not accessible: {e}") + + # Test 3: Properties are read-only + tests_total += 1 + try: + font.family = "Arial" # Should fail + print("✗ FAIL: family should be read-only") + except AttributeError as e: + print(f"✓ PASS: family is read-only: {e}") + tests_passed += 1 + + return tests_passed, tests_total + +def test_property_introspection(): + """Test that properties appear in dir()""" + print("\n=== Testing Property Introspection ===") + + tests_passed = 0 + tests_total = 0 + + # Test Texture properties in dir() + tests_total += 1 + texture = mcrfpy.Texture("assets/kenney_tinydungeon.png", 16, 16) + texture_props = dir(texture) + expected_texture_props = ['sprite_width', 'sprite_height', 'sheet_width', 'sheet_height', 'sprite_count', 'source'] + + missing = [p for p in expected_texture_props if p not in texture_props] + if not missing: + print("✓ PASS: All Texture properties appear in dir()") + tests_passed += 1 + else: + print(f"✗ FAIL: Missing Texture properties in dir(): {missing}") + + # Test Font properties in dir() + tests_total += 1 + font = mcrfpy.Font("assets/JetbrainsMono.ttf") + font_props = dir(font) + expected_font_props = ['family', 'source'] + + missing = [p for p in expected_font_props if p not in font_props] + if not missing: + print("✓ PASS: All Font properties appear in dir()") + tests_passed += 1 + else: + print(f"✗ FAIL: Missing Font properties in dir(): {missing}") + + return tests_passed, tests_total + +def run_test(runtime): + """Timer callback to run the test""" + try: + print("=== Testing Texture and Font Properties (Issue #99) ===\n") + + texture_passed, texture_total = test_texture_properties() + font_passed, font_total = test_font_properties() + intro_passed, intro_total = test_property_introspection() + + total_passed = texture_passed + font_passed + intro_passed + total_tests = texture_total + font_total + intro_total + + print(f"\n=== SUMMARY ===") + print(f"Texture tests: {texture_passed}/{texture_total}") + print(f"Font tests: {font_passed}/{font_total}") + print(f"Introspection tests: {intro_passed}/{intro_total}") + print(f"Total tests passed: {total_passed}/{total_tests}") + + if total_passed == total_tests: + print("\nIssue #99 FIXED: Texture and Font properties exposed successfully!") + print("\nOverall result: PASS") + else: + print("\nIssue #99: Some tests failed") + print("\nOverall result: FAIL") + + except Exception as e: + print(f"\nTest error: {e}") + import traceback + traceback.print_exc() + print("\nOverall result: FAIL") + + sys.exit(0) + +# Set up the test scene +mcrfpy.createScene("test") +mcrfpy.setScene("test") + +# Schedule test to run after game loop starts +mcrfpy.setTimer("test", run_test, 100) \ No newline at end of file