"""
Unit tests for the VertexBufferGroup 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.gpu.buffers.vertex.legacy.VertexBufferGroup`
class, which manages legacy OpenGL vertex buffer objects and mimics VAO functionality.
The tests cover:
- Object initialization and setup
- 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
- Legacy client state management
Dependencies:
- unittest (standard library)
- unittest.mock.MagicMock for OpenGL function mocking
- numpy for test data
- picogl.gpu.buffers.vertex.legacy.VertexBufferGroup
- picogl.buffers.attributes.LayoutDescriptor
To run the tests::
python -m unittest picogl.tests.test_vertex_buffer_group
"""
import ctypes
import unittest
from unittest.mock import MagicMock, call, patch
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, GL_TRIANGLES
from OpenGL.raw.GL.VERSION.GL_1_1 import (
GL_COLOR_ARRAY,
GL_NORMAL_ARRAY,
GL_VERTEX_ARRAY,
)
from OpenGL.raw.GL.VERSION.GL_1_5 import GL_ARRAY_BUFFER, GL_ELEMENT_ARRAY_BUFFER
from picogl.backend.gl.enums import GLDrawMode
from picogl.gpu.buffers.attributes import AttributeSpec, LayoutDescriptor
from picogl.gpu.buffers.vertex.aliases import VertexBufferRole
from picogl.gpu.buffers.vertex.legacy import VertexBufferGroup
from picogl.gpu.buffers.vertex.vbo.vbo_class import VBOType
[docs]
class TestVertexBufferGroup(unittest.TestCase):
"""Test cases for VertexBufferGroup class."""
[docs]
def setUp(self):
"""Set up test fixtures before each test method."""
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)
self.test_colors = np.array(
[[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]], dtype=np.float32
)
# Mock OpenGL functions to avoid context issues
self.gl_patches = [
patch("picogl.gpu.buffers.vertex.legacy.glDrawArrays"),
patch("picogl.gpu.buffers.vertex.legacy.glDrawElements"),
patch("picogl.gpu.buffers.vertex.legacy.glBindBuffer"),
patch("picogl.gpu.buffers.vertex.legacy.glVertexPointer"),
patch("picogl.gpu.buffers.vertex.legacy.glColorPointer"),
patch("picogl.gpu.buffers.vertex.legacy.glNormalPointer"),
patch("picogl.gpu.buffers.vertex.legacy.gl_enableClientState"),
patch("picogl.gpu.buffers.vertex.legacy.legacy_client_states"),
patch("picogl.gpu.buffers.vertex.legacy.delete_buffer_object"),
patch("OpenGL.gl.glGenBuffers", return_value=1),
patch("OpenGL.gl.glBufferData"),
]
# 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(self):
"""Test VertexBufferGroup initialization."""
vbg = VertexBufferGroup()
# Test initial state
self.assertEqual(vbg.handle, 0)
self.assertIsNone(vbg.vao)
self.assertIsNone(vbg.vbo)
self.assertIsNone(vbg.cbo)
self.assertIsNone(vbg.nbo)
self.assertIsNone(vbg.ebo)
self.assertIsNone(vbg.layout)
self.assertEqual(vbg.named_vbos, {})
# Test VBO classes mapping
expected_classes = [
VBOType.VBO,
VBOType.CBO,
VBOType.EBO,
VBOType.NBO,
]
for class_name in expected_classes:
self.assertIn(class_name, vbg.vbo_classes)
[docs]
def test_add_vbo_object(self):
"""Test add_vbo_object method for VBO management."""
vbg = VertexBufferGroup()
mock_vbo = MagicMock()
mock_vbo._id = 123
# Test adding VBO with canonical name
result = vbg.add_vbo_object("position", mock_vbo)
self.assertEqual(result, mock_vbo)
self.assertEqual(vbg.named_vbos["position"], mock_vbo)
# Test adding VBO with alias (if NAME_ALIASES contains mappings)
mock_vbo2 = MagicMock()
mock_vbo2._id = 456
result2 = vbg.add_vbo_object("pos", mock_vbo2)
self.assertEqual(result2, mock_vbo2)
[docs]
def test_get_vbo_object(self):
"""Test get_vbo_object method for VBO retrieval."""
vbg = VertexBufferGroup()
mock_vbo = MagicMock()
vbg.named_vbos["position"] = mock_vbo
result = vbg.get_vbo_object("position")
self.assertEqual(result, mock_vbo)
# Test with non-existent name
result_none = vbg.get_vbo_object("nonexistent")
self.assertIsNone(result_none)
[docs]
def test_index_count_property(self):
"""Test index_count property."""
vbg = VertexBufferGroup()
# Test with no EBO
self.assertEqual(vbg.index_count, 0)
# Test with EBO containing data
mock_ebo = MagicMock()
mock_ebo.data = self.test_indices
vbg.ebo = mock_ebo
self.assertEqual(vbg.index_count, len(self.test_indices))
# Test with EBO but no data attribute
mock_ebo_no_data = MagicMock()
del mock_ebo_no_data.data # Remove data attribute
vbg.ebo = mock_ebo_no_data
self.assertEqual(vbg.index_count, 0)
[docs]
def test_add_vbo(self):
"""Test add_vbo method with different buffer types."""
vbg = VertexBufferGroup()
# Mock the get_buffer_class method to return a mock class
mock_vbo_class = MagicMock()
mock_vbo = MagicMock()
mock_vbo_class.return_value = mock_vbo
with patch.object(vbg, "get_buffer_class", return_value=mock_vbo_class):
# Test adding position VBO with default parameters
result = vbg.add_vbo(VBOType.VBO, data=self.test_data)
# Verify get_buffer_class was called
vbg.get_buffer_class.assert_called_once_with(VBOType.VBO)
# Verify VBO was created and added with default parameters
mock_vbo_class.assert_called_once_with(
data=self.test_data, size=3, handle=None, dtype=GL_FLOAT
)
self.assertIs(result, vbg)
self.assertIs(vbg.named_vbos[VertexBufferRole.VBO], mock_vbo)
[docs]
def test_add_vbo_with_custom_parameters(self):
"""Test add_vbo method with custom parameters."""
vbg = VertexBufferGroup()
# Mock the get_buffer_class method to return a mock class
mock_vbo_class = MagicMock()
mock_vbo = MagicMock()
mock_vbo_class.return_value = mock_vbo
with patch.object(vbg, "get_buffer_class", return_value=mock_vbo_class):
# Test adding VBO with custom parameters
custom_handle = 123
custom_size = 4
custom_dtype = GL_UNSIGNED_INT
result = vbg.add_vbo(
VBOType.VBO,
data=self.test_data,
size=custom_size,
handle=custom_handle,
dtype=custom_dtype,
)
# Verify get_buffer_class was called
vbg.get_buffer_class.assert_called_once_with(VBOType.VBO)
# Verify VBO was created with custom parameters
mock_vbo_class.assert_called_once_with(
data=self.test_data,
size=custom_size,
handle=custom_handle,
dtype=custom_dtype,
)
self.assertIs(result, vbg)
self.assertIs(vbg.named_vbos[VertexBufferRole.VBO], mock_vbo)
[docs]
def test_add_vbo_invalid_parameters(self):
"""Test add_vbo method with invalid parameters."""
vbg = VertexBufferGroup()
# Test with None data
with self.assertRaises(ValueError):
vbg.add_vbo(VBOType.VBO, data=None)
# Test with size <= 0
with self.assertRaises(ValueError):
vbg.add_vbo(VBOType.VBO, data=self.test_data, size=0)
with self.assertRaises(ValueError):
vbg.add_vbo(VBOType.VBO, data=self.test_data, size=-1)
[docs]
def test_add_ebo(self):
"""Test add_ebo method."""
vbg = VertexBufferGroup()
# Mock the EBO class in the vbo_classes dictionary
mock_ebo_class = MagicMock()
mock_ebo = MagicMock()
mock_ebo_class.return_value = mock_ebo
# Replace the EBO class in vbo_classes
original_ebo_class = vbg.vbo_classes[VBOType.EBO]
vbg.vbo_classes[VBOType.EBO] = mock_ebo_class
try:
vbg.add_ebo(VBOType.EBO, data=self.test_indices)
mock_ebo_class.assert_called_once_with(data=self.test_indices)
self.assertEqual(vbg.named_vbos[VBOType.EBO], mock_ebo)
finally:
# Restore original class
vbg.vbo_classes[VBOType.EBO] = original_ebo_class
[docs]
def test_get_buffer_class(self):
"""Test get_buffer_class method."""
vbg = VertexBufferGroup()
# Test known buffer types
self.assertEqual(
vbg.get_buffer_class(VBOType.VBO), vbg.vbo_classes[VBOType.VBO]
)
self.assertEqual(
vbg.get_buffer_class(VBOType.CBO), vbg.vbo_classes[VBOType.CBO]
)
self.assertEqual(
vbg.get_buffer_class(VBOType.EBO), vbg.vbo_classes[VBOType.EBO]
)
self.assertEqual(
vbg.get_buffer_class(VBOType.NBO), vbg.vbo_classes[VBOType.NBO]
)
# Test unknown buffer type (should return default)
self.assertEqual(vbg.get_buffer_class("unknown"), vbg.vbo_classes[VBOType.VBO])
[docs]
def test_draw_with_arrays(self):
"""Test draw method with vertex arrays."""
vbg = VertexBufferGroup()
with patch(
"picogl.gpu.buffers.vertex.legacy.legacy_client_states"
) as mock_client_states, patch(
"picogl.gpu.buffers.vertex.legacy.glDrawArrays"
) as mock_draw_arrays:
mock_client_states.return_value.__enter__ = MagicMock()
mock_client_states.return_value.__exit__ = MagicMock()
vbg.draw(index_count=10, mode=GLDrawMode.POINTS)
mock_draw_arrays.assert_called_once_with(GLDrawMode.POINTS, 0, 10)
mock_client_states.assert_called_once_with(
GL_VERTEX_ARRAY, GL_COLOR_ARRAY, GL_NORMAL_ARRAY
)
[docs]
def test_draw_with_auto_index_count(self):
"""Test draw method with automatic index count."""
vbg = VertexBufferGroup()
mock_ebo = MagicMock()
mock_ebo.data = self.test_indices
vbg.ebo = mock_ebo
with patch(
"picogl.gpu.buffers.vertex.legacy.legacy_client_states"
) as mock_client_states, patch(
"picogl.gpu.buffers.vertex.legacy.glDrawArrays"
) as mock_draw_arrays:
mock_client_states.return_value.__enter__ = MagicMock()
mock_client_states.return_value.__exit__ = MagicMock()
vbg.draw(mode=GLDrawMode.POINTS)
mock_draw_arrays.assert_called_once_with(
GLDrawMode.POINTS, 0, len(self.test_indices)
)
[docs]
def test_draw_elements(self):
"""Test draw_elements method."""
vbg = VertexBufferGroup()
mock_ebo = MagicMock()
mock_ebo._id = 123
mock_ebo.handle = 123
vbg.ebo = mock_ebo
with patch(
"picogl.gpu.buffers.vertex.legacy.legacy_client_states"
) as mock_client_states, patch(
"picogl.gpu.buffers.vertex.legacy.glDrawElements"
) as mock_draw_elements:
mock_client_states.return_value.__enter__ = MagicMock()
mock_client_states.return_value.__exit__ = MagicMock()
vbg.draw_elements(count=5, mode=GLDrawMode.TRIANGLES)
mock_draw_elements.assert_called_once()
mock_client_states.assert_called_once_with(
GL_VERTEX_ARRAY, GL_COLOR_ARRAY, GL_NORMAL_ARRAY
)
[docs]
def test_draw_elements_without_ebo(self):
"""Test draw_elements method without EBO raises error."""
vbg = VertexBufferGroup()
with self.assertRaises(RuntimeError) as context:
vbg.draw_elements()
self.assertIn("No element buffer (EBO) bound", str(context.exception))
[docs]
def test_draw_elements_with_auto_count(self):
"""Test draw_elements method with automatic count."""
vbg = VertexBufferGroup()
mock_ebo = MagicMock()
mock_ebo._id = 123
mock_ebo.handle = 123
mock_ebo.data = self.test_indices
vbg.ebo = mock_ebo
with patch(
"picogl.gpu.buffers.vertex.legacy.legacy_client_states"
) as mock_client_states, patch(
"picogl.gpu.buffers.vertex.legacy.glDrawElements"
) as mock_draw_elements:
mock_client_states.return_value.__enter__ = MagicMock()
mock_client_states.return_value.__exit__ = MagicMock()
vbg.draw_elements()
mock_draw_elements.assert_called_once()
[docs]
def test_set_layout(self):
"""Test set_layout method."""
vbg = VertexBufferGroup()
layout = LayoutDescriptor(attributes=[])
vbg.set_layout(layout)
self.assertEqual(vbg.layout, layout)
[docs]
def test_bind_with_layout(self):
"""Test bind method with layout."""
vbg = VertexBufferGroup()
# 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])
vbg.layout = layout
# Add mock VBO
mock_vbo = MagicMock()
mock_vbo._id = 123
vbg.named_vbos["position"] = mock_vbo
vbg.bind()
# The bind method calls glBindBuffer and glVertexAttribPointer
# which are already mocked in setUp, so we just verify no errors occurred
[docs]
def test_bind_without_layout(self):
"""Test bind method without layout."""
vbg = VertexBufferGroup()
# Should return early without processing
vbg.bind()
# No assertions needed - should not raise any errors
[docs]
def test_bind_with_missing_vbo(self):
"""Test bind method with missing VBO in layout."""
vbg = VertexBufferGroup()
# Create a layout with attribute that has no corresponding VBO
attr_spec = AttributeSpec(
name="nonexistent",
index=0,
size=3,
type=GL_FLOAT,
normalized=False,
stride=0,
offset=0,
)
layout = LayoutDescriptor(attributes=[attr_spec])
vbg.layout = layout
# Should not raise error, just skip missing VBO
vbg.bind()
[docs]
def test_unbind_with_layout(self):
"""Test unbind method with layout."""
vbg = VertexBufferGroup()
# 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])
vbg.layout = layout
vbg.unbind()
# The unbind method should disable vertex attrib arrays
# This is already mocked in setUp
[docs]
def test_unbind_without_layout(self):
"""Test unbind method without layout."""
vbg = VertexBufferGroup()
# Should return early without processing
vbg.unbind()
# No assertions needed - should not raise any errors
[docs]
def test_delete(self):
"""Test delete method."""
vbg = VertexBufferGroup()
# Add some mock buffers
mock_vbo = MagicMock()
mock_cbo = MagicMock()
mock_nbo = MagicMock()
mock_ebo = MagicMock()
vbg.vbo = mock_vbo
vbg.cbo = mock_cbo
vbg.nbo = mock_nbo
vbg.ebo = mock_ebo
# Set up layout
layout = LayoutDescriptor(attributes=[])
vbg.layout = layout
vbg.delete()
# Verify buffers were deleted
# The delete_buffer_object calls are mocked in setUp
# Verify internal state was cleared
self.assertIsNone(vbg.nbo)
self.assertIsNone(vbg.cbo)
self.assertIsNone(vbg.vbo)
self.assertIsNone(vbg.ebo)
self.assertIsNone(vbg.layout)
[docs]
def test_delete_with_none_buffers(self):
"""Test delete method with None buffers."""
vbg = VertexBufferGroup()
# All buffers are None by default
vbg.delete()
# Should not raise any errors
self.assertIsNone(vbg.vbo)
self.assertIsNone(vbg.cbo)
self.assertIsNone(vbg.nbo)
self.assertIsNone(vbg.ebo)
[docs]
def test_context_manager(self):
"""Test that VertexBufferGroup can be used as a context manager."""
vbg = VertexBufferGroup()
# Test that __enter__ and __exit__ methods exist
self.assertTrue(hasattr(vbg, "__enter__"))
self.assertTrue(hasattr(vbg, "__exit__"))
# Test context manager usage
with patch.object(vbg, "bind") as mock_bind, patch.object(
vbg, "unbind"
) as mock_unbind:
with vbg as context_vbg:
self.assertEqual(context_vbg, vbg)
mock_bind.assert_called_once()
mock_unbind.assert_called_once()
[docs]
def test_error_handling_in_add_vbo(self):
"""Test error handling in add_vbo method."""
vbg = VertexBufferGroup()
# Mock an exception during VBO creation by mocking get_buffer_class
mock_vbo_class = MagicMock()
mock_vbo_class.side_effect = Exception("VBO creation error")
with patch.object(vbg, "get_buffer_class", return_value=mock_vbo_class):
# The new add_vbo method should let exceptions propagate
with self.assertRaises(Exception) as context:
vbg.add_vbo(VBOType.VBO, data=self.test_data)
# Verify the exception message
self.assertIn("VBO creation error", str(context.exception))
[docs]
def test_error_handling_in_bind(self):
"""Test error handling in bind method."""
vbg = VertexBufferGroup()
# Create a layout that will cause an error
attr_spec = AttributeSpec(
name="position",
index=0,
size=3,
type=GL_FLOAT,
normalized=False,
stride=0,
offset=0,
)
layout = LayoutDescriptor(attributes=[attr_spec])
vbg.layout = layout
# Add mock VBO that will cause an error
mock_vbo = MagicMock()
mock_vbo.handle = 123
vbg.named_vbos[VertexBufferRole.VBO] = mock_vbo
with patch(
"picogl.gpu.buffers.vertex.legacy.glVertexPointer",
side_effect=Exception("OpenGL error"),
):
with patch("picogl.gpu.buffers.vertex.legacy.log.error") as mock_log_error:
vbg.bind()
mock_log_error.assert_called_once()
self.assertIn("error", mock_log_error.call_args[0][0])
[docs]
def test_bind_without_ebo(self):
"""bind() skips EBO when geometry uses glDrawArrays only."""
vbg = VertexBufferGroup()
mock_vbo = MagicMock()
mock_vbo.handle = 42
vbg.named_vbos[VertexBufferRole.VBO] = mock_vbo
vbg.layout = LayoutDescriptor(
attributes=[
AttributeSpec(
name=VertexBufferRole.VBO,
index=0,
size=3,
type=GL_FLOAT,
normalized=False,
stride=0,
offset=0,
)
]
)
with patch("picogl.gpu.buffers.vertex.legacy.glVertexPointer"), patch(
"picogl.gpu.buffers.vertex.legacy.gl_enableClientState"
), patch("picogl.gpu.buffers.vertex.legacy.glBindBuffer") as mock_bind_buffer:
vbg.bind()
mock_bind_buffer.assert_called()
[docs]
def test_bind_legacy_buffer_roles(self):
"""bind() resolves pointer method names and VertexBufferRole attributes."""
vbg = VertexBufferGroup()
mock_vbo = MagicMock()
mock_vbo.handle = 42
vbg.named_vbos[VertexBufferRole.VBO] = mock_vbo
attr_spec = AttributeSpec(
name=VertexBufferRole.VBO,
index=0,
size=3,
type=GL_FLOAT,
normalized=False,
stride=12,
offset=0,
)
vbg.layout = LayoutDescriptor(attributes=[attr_spec])
with patch(
"picogl.gpu.buffers.vertex.legacy.glVertexPointer"
) as mock_vertex_pointer, patch(
"picogl.gpu.buffers.vertex.legacy.gl_enableClientState"
) as mock_enable_state:
vbg.bind()
mock_enable_state.assert_called_once_with(GL_VERTEX_ARRAY)
mock_vertex_pointer.assert_called_once()
args = mock_vertex_pointer.call_args[0]
self.assertEqual(args[:3], (3, GL_FLOAT, 12))
self.assertIsInstance(args[3], ctypes.c_void_p)
[docs]
def test_bind_uses_vbo_handle(self):
"""bind() passes VBO handle to glBindBuffer, not the buffer object."""
vbg = VertexBufferGroup()
mock_vbo = MagicMock()
mock_vbo.handle = 77
vbg.named_vbos[VertexBufferRole.VBO] = mock_vbo
vbg.layout = LayoutDescriptor(
attributes=[
AttributeSpec(
name=VertexBufferRole.VBO,
index=0,
size=3,
type=GL_FLOAT,
normalized=False,
stride=0,
offset=0,
)
]
)
with patch("picogl.gpu.buffers.vertex.legacy.glVertexPointer"), patch(
"picogl.gpu.buffers.vertex.legacy.gl_enableClientState"
), patch("picogl.gpu.buffers.vertex.legacy.glBindBuffer") as mock_bind_buffer:
vbg.bind()
mock_bind_buffer.assert_any_call(GL_ARRAY_BUFFER, 77)
[docs]
def test_bind_resolves_role_from_attr_role(self):
"""bind() uses AttributeSpec.role as the canonical buffer role."""
vbg = VertexBufferGroup()
mock_cbo = MagicMock()
mock_cbo.handle = 55
vbg.named_vbos[VertexBufferRole.CBO] = mock_cbo
vbg.layout = LayoutDescriptor(
attributes=[
AttributeSpec(
name="misleading_name",
index=2,
size=3,
type=GL_FLOAT,
normalized=False,
stride=0,
offset=0,
role=VertexBufferRole.CBO,
)
]
)
with patch(
"picogl.gpu.buffers.vertex.legacy.glColorPointer"
) as mock_color_pointer, patch(
"picogl.gpu.buffers.vertex.legacy.gl_enableClientState"
), patch(
"picogl.gpu.buffers.vertex.legacy.glBindBuffer"
):
vbg.bind()
mock_color_pointer.assert_called_once()
[docs]
def test_bind_resolves_role_from_vbo_type(self):
"""bind() resolves buffer role from AttributeSpec.vbo_type."""
vbg = VertexBufferGroup()
mock_cbo = MagicMock()
mock_cbo.handle = 55
vbg.named_vbos[VertexBufferRole.CBO] = mock_cbo
vbg.layout = LayoutDescriptor(
attributes=[
AttributeSpec(
name="colors",
index=2,
size=3,
type=GL_FLOAT,
normalized=False,
stride=0,
offset=0,
vbo_type=VBOType.CBO,
role=VertexBufferRole.CBO,
)
]
)
with patch(
"picogl.gpu.buffers.vertex.legacy.glColorPointer"
) as mock_color_pointer, patch(
"picogl.gpu.buffers.vertex.legacy.gl_enableClientState"
), patch(
"picogl.gpu.buffers.vertex.legacy.glBindBuffer"
):
vbg.bind()
mock_color_pointer.assert_called_once()
[docs]
def test_compat_properties_sync_named_vbos(self):
"""vbo/cbo/nbo/ebo properties read and write named_vbos."""
vbg = VertexBufferGroup()
mock_vbo = MagicMock()
mock_cbo = MagicMock()
vbg.add_vbo_object(VBOType.VBO, mock_vbo)
vbg.cbo = mock_cbo
self.assertIs(vbg.vbo, mock_vbo)
self.assertIs(vbg.named_vbos[VertexBufferRole.VBO], mock_vbo)
self.assertIs(vbg.cbo, mock_cbo)
self.assertIs(vbg.named_vbos[VertexBufferRole.CBO], mock_cbo)
replacement = MagicMock()
vbg.vbo = replacement
self.assertIs(vbg.named_vbos[VertexBufferRole.VBO], replacement)
vbg.vbo = None
self.assertIsNone(vbg.vbo)
self.assertNotIn(VertexBufferRole.VBO, vbg.named_vbos)
[docs]
def test_draw_elements_uses_layout_bind_only(self):
"""draw_elements() relies on layout bind(), not per-VBO bind()."""
vbg = VertexBufferGroup()
mock_ebo = MagicMock()
mock_ebo.handle = 99
vbg.ebo = mock_ebo
mock_vbo = MagicMock()
mock_vbo.handle = 42
vbg.named_vbos[VertexBufferRole.VBO] = mock_vbo
vbg.layout = LayoutDescriptor(
attributes=[
AttributeSpec(
name=VertexBufferRole.VBO,
index=0,
size=3,
type=GL_FLOAT,
normalized=False,
stride=0,
offset=0,
)
]
)
with patch(
"picogl.gpu.buffers.vertex.legacy.legacy_client_states"
) as mock_client_states, patch(
"picogl.gpu.buffers.vertex.legacy.glDrawElements"
):
mock_client_states.return_value.__enter__ = MagicMock()
mock_client_states.return_value.__exit__ = MagicMock()
vbg.draw_elements(count=3, mode=GLDrawMode.TRIANGLES)
mock_vbo.bind.assert_not_called()
[docs]
def test_type_conversion_in_bind(self):
"""Test type conversion handling in bind method."""
vbg = VertexBufferGroup()
# Create a layout with various type scenarios
attr_spec = AttributeSpec(
name="position",
index=0,
size=3,
type=GL_FLOAT,
normalized=False,
stride=0,
offset=0,
)
layout = LayoutDescriptor(attributes=[attr_spec])
vbg.layout = layout
# Add mock VBO
mock_vbo = MagicMock()
mock_vbo.handle = 123
vbg.named_vbos[VertexBufferRole.VBO] = mock_vbo
with patch(
"picogl.gpu.buffers.vertex.legacy.glVertexPointer"
) as mock_vertex_pointer:
vbg.bind()
mock_vertex_pointer.assert_called_once()
[docs]
def test_repr_string(self):
"""Test string representation of VertexBufferGroup."""
vbg = VertexBufferGroup()
repr_str = repr(vbg)
self.assertIn("VertexBufferGroup", repr_str)
# The default repr doesn't include specific attributes, just test that it's a valid repr
self.assertTrue(repr_str.startswith("<"))
self.assertTrue(repr_str.endswith(">"))
[docs]
def test_vbo_classes_mapping(self):
"""Test VBO classes mapping is correct."""
vbg = VertexBufferGroup()
expected_mappings = {
VBOType.VBO: "LegacyPositionVBO",
VBOType.CBO: "LegacyColorVBO",
VBOType.EBO: "LegacyEBO",
VBOType.NBO: "LegacyNormalVBO",
}
for key, expected_class_name in expected_mappings.items():
vbo_class = vbg.vbo_classes[key]
self.assertEqual(vbo_class.__name__, expected_class_name)
[docs]
def test_resolve_client_states(self):
"""_resolve_client_states() derives gl client states from layout roles."""
vbg = VertexBufferGroup()
vbg.named_vbos[VertexBufferRole.VBO] = MagicMock()
vbg.named_vbos[VertexBufferRole.CBO] = MagicMock()
vbg.layout = LayoutDescriptor(
attributes=[
AttributeSpec(
name=VertexBufferRole.VBO,
index=0,
size=3,
type=GL_FLOAT,
normalized=False,
stride=0,
offset=0,
),
AttributeSpec(
name=VertexBufferRole.CBO,
index=2,
size=3,
type=GL_FLOAT,
normalized=False,
stride=0,
offset=0,
),
]
)
states = vbg._resolve_client_states()
self.assertEqual(states, (GL_VERTEX_ARRAY, GL_COLOR_ARRAY))
[docs]
def test_legacy_client_states_integration(self):
"""Test integration with legacy client states."""
vbg = VertexBufferGroup()
# Add some mock VBOs
mock_vbo = MagicMock()
vbg.named_vbos = {VBOType.VBO: mock_vbo}
with patch(
"picogl.gpu.buffers.vertex.legacy.legacy_client_states"
) as mock_client_states:
mock_client_states.return_value.__enter__ = MagicMock()
mock_client_states.return_value.__exit__ = MagicMock()
vbg.draw()
# Verify legacy client states were called with correct parameters
mock_client_states.assert_called_once_with(
GL_VERTEX_ARRAY, GL_COLOR_ARRAY, GL_NORMAL_ARRAY
)
if __name__ == "__main__":
unittest.main()