Source code for picogl.tests.test_vertex_array_object

"""
Unit tests for the VertexArrayObject class in the PicoGL OpenGL backend.

This module contains a comprehensive suite of unit tests for verifying the correctness,
robustness, and interface of the :class:`picogl.backend.modern.core.vertex.array.object.VertexArrayObject`
class, which manages OpenGL Vertex Array Objects (VAOs) in modern OpenGL rendering workflows.

The tests cover:

- Object initialization with and without handle parameter
- VAO binding/unbinding operations
- VBO management and attribute configuration
- EBO (Element Buffer Object) management
- Layout descriptor configuration
- Drawing operations with various modes
- Buffer cleanup and memory management
- Error handling and edge cases

Dependencies:
    - unittest (standard library)
    - unittest.mock.MagicMock for OpenGL function mocking
    - numpy for test data
    - picogl.backend.modern.core.vertex.array.object.VertexArrayObject
    - picogl.buffers.attributes.LayoutDescriptor

To run the tests::

    python -m unittest picogl.tests.test_vertex_array_object

"""

import ctypes
import unittest
from unittest.mock import MagicMock, patch, call

import numpy as np
from OpenGL.raw.GL._types import GL_FLOAT, GL_UNSIGNED_INT
from OpenGL.raw.GL.VERSION.GL_1_0 import GL_POINTS
from OpenGL.raw.GL.VERSION.GL_1_5 import GL_ELEMENT_ARRAY_BUFFER, GL_STATIC_DRAW, GL_ARRAY_BUFFER

from picogl.backend.modern.core.vertex.array.object import VertexArrayObject
from picogl.buffers.attributes import LayoutDescriptor, AttributeSpec


[docs] class TestVertexArrayObject(unittest.TestCase): """Test cases for VertexArrayObject class."""
[docs] def setUp(self): """Set up test fixtures before each test method.""" self.mock_handle = 123 self.test_data = np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]], dtype=np.float32) self.test_indices = np.array([0, 1, 2], dtype=np.uint32) # Mock OpenGL functions to avoid context issues self.gl_patches = [ patch('picogl.backend.modern.core.vertex.array.object.glBindVertexArray'), patch('picogl.backend.modern.core.vertex.array.object.glGenVertexArrays'), patch('picogl.backend.modern.core.vertex.array.object.glDeleteVertexArrays'), patch('picogl.backend.modern.core.vertex.array.object.glBindBuffer'), patch('picogl.backend.modern.core.vertex.array.object.glEnableVertexAttribArray'), patch('picogl.backend.modern.core.vertex.array.object.glVertexAttribPointer'), patch('picogl.backend.modern.core.vertex.array.object.glDrawArrays'), patch('picogl.backend.modern.core.vertex.array.object.glDrawElements'), patch('picogl.backend.modern.core.vertex.array.object.enable_points_rendering_state'), patch('picogl.backend.modern.core.vertex.array.object.gl_gen_safe'), ] # Start all patches for patch_obj in self.gl_patches: patch_obj.start()
[docs] def tearDown(self): """Clean up after each test method.""" # Stop all patches for patch_obj in self.gl_patches: patch_obj.stop()
[docs] def test_initialization_with_handle(self): """Test VertexArrayObject initialization with provided handle.""" vao = VertexArrayObject(handle=self.mock_handle) self.assertEqual(vao.handle, self.mock_handle) self.assertFalse(vao._configured) self.assertEqual(vao.attributes, []) self.assertEqual(vao.vbos, []) self.assertEqual(vao.named_vbos, {}) self.assertIsNone(vao.layout)
[docs] def test_initialization_without_handle(self): """Test VertexArrayObject initialization without handle (auto-generate).""" # Mock glGenVertexArrays to return a handle with patch('picogl.backend.modern.core.vertex.array.object.glGenVertexArrays') as mock_gen: with patch('picogl.backend.modern.core.vertex.array.object.gl_gen_safe') as mock_gen_safe: mock_gen_safe.return_value = self.mock_handle vao = VertexArrayObject() self.assertEqual(vao.handle, self.mock_handle) mock_gen_safe.assert_called_once()
[docs] def test_initialization_raises_error_when_no_context(self): """Test that initialization raises error when OpenGL context is not ready.""" with patch('picogl.backend.modern.core.vertex.array.object.glGenVertexArrays') as mock_gen: with patch('picogl.backend.modern.core.vertex.array.object.gl_gen_safe') as mock_gen_safe: mock_gen.return_value = None mock_gen_safe.side_effect = TypeError("int() argument must be a string, a bytes-like object or a real number, not 'NoneType'") with self.assertRaises(TypeError) as context: VertexArrayObject() self.assertIn("int() argument must be a string", str(context.exception))
[docs] def test_bind(self): """Test bind method.""" vao = VertexArrayObject(handle=self.mock_handle) vao.bind()
# The bind call is already mocked in setUp
[docs] def test_unbind(self): """Test unbind method.""" vao = VertexArrayObject(handle=self.mock_handle) vao.unbind()
# The unbind call is already mocked in setUp
[docs] def test_delete(self): """Test delete method.""" vao = VertexArrayObject(handle=self.mock_handle) vao.delete()
# The delete call is already mocked in setUp
[docs] def test_add_vbo(self): """Test add_vbo method.""" with patch('picogl.backend.modern.core.vertex.array.object.ModernVBO') as mock_vbo_class: mock_vbo = MagicMock() mock_vbo.handle = 456 mock_vbo_class.return_value = mock_vbo vao = VertexArrayObject(handle=self.mock_handle) result = vao.add_vbo( index=0, data=self.test_data, size=3, dtype=GL_FLOAT, name="position" ) # Verify VBO was created and configured mock_vbo_class.assert_called_once_with(handle=None) mock_vbo.bind.assert_called_once() mock_vbo.set_data.assert_called_once_with(self.test_data) mock_vbo.set_vertex_attributes.assert_called_once_with( index=0, data=self.test_data, size=3, dtype=GL_FLOAT ) mock_vbo.configure.assert_called_once() # Verify VBO was added to internal lists self.assertEqual(len(vao.attributes), 1) self.assertEqual(len(vao.vbos), 1) self.assertEqual(vao.named_vbos["position"], mock_vbo) self.assertEqual(result, mock_vbo)
[docs] def test_add_attribute(self): """Test add_attribute method.""" vao = VertexArrayObject(handle=self.mock_handle) vbo_handle = 789 vao.add_attribute( index=1, vbo=vbo_handle, size=3, dtype=GL_FLOAT, normalized=False, stride=0, offset=0 ) # Verify attribute was added to the list self.assertEqual(len(vao.attributes), 1) expected_attr = (1, vbo_handle, 3, GL_FLOAT, False, 0, 0) self.assertEqual(vao.attributes[0], expected_attr)
[docs] def test_add_ebo(self): """Test add_ebo method.""" with patch('picogl.backend.modern.core.vertex.array.object.ModernEBO') as mock_ebo_class: mock_ebo = MagicMock() mock_ebo_class.return_value = mock_ebo vao = VertexArrayObject(handle=self.mock_handle) result = vao.add_ebo(self.test_indices) # Verify EBO was created and configured mock_ebo_class.assert_called_once_with(data=self.test_indices) mock_ebo.bind.assert_called_once() mock_ebo.set_element_attributes.assert_called_once_with( data=self.test_indices, size=self.test_indices.nbytes, dtype=GL_STATIC_DRAW ) mock_ebo.configure.assert_called_once() # Verify EBO was stored self.assertEqual(vao.ebo, mock_ebo) self.assertEqual(result, mock_ebo)
[docs] def test_set_ebo(self): """Test set_ebo method.""" with patch('picogl.backend.modern.core.vertex.array.object.ModernEBO') as mock_ebo_class: mock_ebo = MagicMock() mock_ebo_class.return_value = mock_ebo ebo_handle = 999 vao = VertexArrayObject(handle=self.mock_handle) result = vao.set_ebo(ebo_handle) # Verify EBO was created with the handle mock_ebo_class.assert_called_once_with(handle=ebo_handle) mock_ebo.bind.assert_called_once() # Verify EBO was stored self.assertEqual(vao.ebo, mock_ebo) self.assertEqual(result, ebo_handle)
[docs] def test_set_layout(self): """Test set_layout method with LayoutDescriptor.""" # Create a mock layout descriptor attr_spec = AttributeSpec( name="position", index=0, size=3, type=GL_FLOAT, normalized=False, stride=0, offset=0 ) layout = LayoutDescriptor(attributes=[attr_spec]) # Create mock VBO and EBO mock_vbo = MagicMock() mock_vbo._id = 100 mock_ebo = MagicMock() mock_ebo._id = 200 vao = VertexArrayObject(handle=self.mock_handle) vao.vao = mock_vbo # Set vao to enable layout processing vao.vbo = mock_vbo vao.ebo = mock_ebo vao.set_layout(layout) # Verify layout was stored self.assertEqual(vao.layout, layout) self.assertTrue(vao._configured)
[docs] def test_set_layout_with_none_vao(self): """Test set_layout method when vao is None.""" layout = LayoutDescriptor(attributes=[]) vao = VertexArrayObject(handle=self.mock_handle) vao.vao = None vao.set_layout(layout) # Should return early without processing self.assertEqual(vao.layout, layout)
[docs] def test_add_vbo_object(self): """Test add_vbo_object method for VBO management.""" vao = VertexArrayObject(handle=self.mock_handle) mock_vbo = MagicMock() # Test adding VBO with canonical name result = vao.add_vbo_object("position", mock_vbo) self.assertEqual(result, mock_vbo) self.assertEqual(vao.named_vbos["position"], mock_vbo) # Test adding VBO with alias mock_vbo2 = MagicMock() result2 = vao.add_vbo_object("pos", mock_vbo2) # "pos" should be an alias for "position"
# Note: This test assumes NAME_ALIASES contains "pos" -> "position" mapping # The actual behavior depends on the NAME_ALIASES dictionary
[docs] def test_get_vbo_object(self): """Test get_vbo_object method for VBO retrieval.""" vao = VertexArrayObject(handle=self.mock_handle) mock_vbo = MagicMock() vao.named_vbos["position"] = mock_vbo result = vao.get_vbo_object("position") self.assertEqual(result, mock_vbo) # Test with non-existent name result_none = vao.get_vbo_object("nonexistent") self.assertIsNone(result_none)
[docs] def test_index_count_property(self): """Test index_count property.""" vao = VertexArrayObject(handle=self.mock_handle) # Test with no EBO self.assertEqual(vao.index_count, 0) # Test with EBO containing data mock_ebo = MagicMock() mock_ebo.data = self.test_indices vao.ebo = mock_ebo self.assertEqual(vao.index_count, len(self.test_indices))
[docs] def test_draw_with_arrays(self): """Test draw method with vertex arrays (no EBO).""" vao = VertexArrayObject(handle=self.mock_handle) vao.ebo = None # No EBO, should use glDrawArrays vao.draw(index_count=10, mode=GL_POINTS)
# The draw calls are already mocked in setUp
[docs] def test_draw_with_elements(self): """Test draw method with element arrays (EBO present).""" vao = VertexArrayObject(handle=self.mock_handle) mock_ebo = MagicMock() mock_ebo.data = self.test_indices vao.ebo = mock_ebo vao.draw(index_count=5, mode=GL_POINTS)
# The draw calls are already mocked in setUp
[docs] def test_draw_with_auto_index_count(self): """Test draw method with automatic index count from EBO.""" vao = VertexArrayObject(handle=self.mock_handle) mock_ebo = MagicMock() mock_ebo.data = self.test_indices vao.ebo = mock_ebo vao.draw() # No index_count provided, should use EBO data length
# The draw calls are already mocked in setUp
[docs] def test_delete_buffers(self): """Test delete_buffers method.""" with patch('picogl.backend.modern.core.vertex.array.object.delete_buffer') as mock_delete: vao = VertexArrayObject(handle=self.mock_handle) # Add some mock VBOs mock_vbo1 = MagicMock() mock_vbo2 = MagicMock() vao.vbos = [mock_vbo1, mock_vbo2] # Add mock EBO mock_ebo = MagicMock() vao.ebo = mock_ebo vao.delete_buffers() # Verify VBOs were deleted self.assertEqual(mock_delete.call_count, 3) # 2 VBOs + 1 EBO mock_delete.assert_any_call(mock_vbo1) mock_delete.assert_any_call(mock_vbo2) mock_delete.assert_any_call(mock_ebo) # Verify internal state was cleared self.assertEqual(len(vao.vbos), 0) self.assertIsNone(vao.ebo) self.assertEqual(len(vao.named_vbos), 0)
[docs] def test_context_manager(self): """Test that VertexArrayObject can be used as a context manager.""" vao = VertexArrayObject(handle=self.mock_handle) # Test that __enter__ and __exit__ methods exist self.assertTrue(hasattr(vao, "__enter__")) self.assertTrue(hasattr(vao, "__exit__")) # Test context manager usage with patch.object(vao, 'bind') as mock_bind, patch.object(vao, 'unbind') as mock_unbind: with vao as context_vao: self.assertEqual(context_vao, vao) mock_bind.assert_called_once() mock_unbind.assert_called_once()
[docs] def test_repr_string(self): """Test string representation of VertexArrayObject.""" vao = VertexArrayObject(handle=self.mock_handle) repr_str = repr(vao) self.assertIn("VertexArrayObject", repr_str) # The default repr doesn't include the handle, just test that it's a valid repr self.assertTrue(repr_str.startswith('<')) self.assertTrue(repr_str.endswith('>'))
[docs] def test_error_handling_in_set_layout(self): """Test error handling in set_layout method.""" layout = LayoutDescriptor(attributes=[]) vao = VertexArrayObject(handle=self.mock_handle) vao.vao = MagicMock() # Set vao to enable processing # Mock an exception during OpenGL calls with patch('picogl.backend.modern.core.vertex.array.object.glBindVertexArray', side_effect=Exception("OpenGL error")): with patch('picogl.logger.Logger.error') as mock_log_error: vao.set_layout(layout) mock_log_error.assert_called_once() self.assertIn("error", mock_log_error.call_args[0][0])
[docs] def test_error_handling_in_index_count(self): """Test error handling in index_count property.""" vao = VertexArrayObject(handle=self.mock_handle) # Mock an exception with patch('picogl.logger.Logger.error') as mock_log_error: # Create a property that raises an exception class MockEBO: @property def data(self): raise Exception("Data access error") vao.ebo = MockEBO() result = vao.index_count self.assertIsNone(result) # Should return None on error (as per the actual implementation) mock_log_error.assert_called_once()
if __name__ == "__main__": unittest.main()