Source code for picogl.tests.test_meshdata

"""
Unit tests for the MeshData 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.renderer.meshdata.MeshData`
class, which manages OpenGL mesh data and rendering state.

The tests cover:

- Object initialization with various buffer types
- from_raw class method with different input scenarios
- OpenGL binding/unbinding operations
- Drawing operations with various modes and parameters
- Context manager functionality
- Utility methods and data conversion
- Error handling and edge cases
- Data validation and type conversion

Dependencies:
    - unittest (standard library)
    - unittest.mock.MagicMock for OpenGL function mocking
    - numpy for test data
    - picogl.renderer.meshdata.MeshData

To run the tests::

    python -m unittest picogl.tests.test_meshdata

"""

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

import numpy as np
from OpenGL import GL

from picogl.renderer.meshdata import MeshData


[docs] class TestMeshData(unittest.TestCase): """Test cases for MeshData class."""
[docs] def setUp(self): """Set up test fixtures before each test method.""" # Create test data self.test_vertices = np.array([ [0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [1.0, 1.0, 0.0] ], dtype=np.float32) self.test_normals = np.array([ [0.0, 0.0, 1.0], [0.0, 0.0, 1.0], [0.0, 0.0, 1.0], [0.0, 0.0, 1.0] ], dtype=np.float32) self.test_colors = np.array([ [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0], [1.0, 1.0, 0.0] ], dtype=np.float32) self.test_uvs = np.array([ [0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0] ], dtype=np.float32) self.test_indices = np.array([0, 1, 2, 1, 3, 2], dtype=np.int32) # Mock OpenGL functions to avoid context issues self.gl_patches = [ patch('picogl.renderer.meshdata.GL.glEnableClientState'), patch('picogl.renderer.meshdata.GL.glDisableClientState'), patch('picogl.renderer.meshdata.GL.glVertexPointer'), patch('picogl.renderer.meshdata.GL.glNormalPointer'), patch('picogl.renderer.meshdata.GL.glColorPointer'), patch('picogl.renderer.meshdata.GL.glTexCoordPointer'), patch('picogl.renderer.meshdata.GL.glDrawElements'), patch('picogl.renderer.meshdata.GL.glLineWidth'), patch('picogl.renderer.meshdata.GL.glEnable'), patch('picogl.renderer.meshdata.GL.glDisable'), patch('picogl.renderer.meshdata.GL.glBlendFunc'), patch('picogl.renderer.meshdata.GL.glColor4f'), patch('picogl.renderer.meshdata.GL.glPolygonMode'), ] # 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_all_buffers(self): """Test MeshData initialization with all buffer types.""" mesh = MeshData( vbo=self.test_vertices, nbo=self.test_normals, uvs=self.test_uvs, cbo=self.test_colors, ebo=self.test_indices ) # Test buffer assignments np.testing.assert_array_equal(mesh.vbo, self.test_vertices) np.testing.assert_array_equal(mesh.nbo, self.test_normals) np.testing.assert_array_equal(mesh.uvs, self.test_uvs) np.testing.assert_array_equal(mesh.cbo, self.test_colors) np.testing.assert_array_equal(mesh.ebo, self.test_indices) # Test vertex count calculation expected_vertex_count = len(self.test_vertices.flatten()) // 3 self.assertEqual(mesh.vertex_count, expected_vertex_count)
[docs] def test_initialization_with_minimal_data(self): """Test MeshData initialization with only vertices.""" mesh = MeshData(vbo=self.test_vertices) self.assertIsNotNone(mesh.vbo) self.assertIsNone(mesh.nbo) self.assertIsNone(mesh.uvs) self.assertIsNone(mesh.cbo) self.assertIsNone(mesh.ebo) expected_vertex_count = len(self.test_vertices.flatten()) // 3 self.assertEqual(mesh.vertex_count, expected_vertex_count)
[docs] def test_initialization_with_none_vertices(self): """Test MeshData initialization with None vertices.""" mesh = MeshData() self.assertIsNone(mesh.vbo) self.assertIsNone(mesh.nbo) self.assertIsNone(mesh.uvs) self.assertIsNone(mesh.cbo) self.assertIsNone(mesh.ebo) self.assertIsNone(mesh.vertex_count)
[docs] def test_as_ribbon_args(self): """Test as_ribbon_args method.""" mesh = MeshData( vbo=self.test_vertices, nbo=self.test_normals, cbo=self.test_colors, ebo=self.test_indices ) ribbon_args = mesh.as_ribbon_args() expected = { "positions": self.test_vertices, "colors": self.test_colors, "normals": self.test_normals, "indices": self.test_indices, } self.assertEqual(ribbon_args, expected)
[docs] def test_str_representation(self): """Test string representation of MeshData.""" mesh = MeshData( vbo=self.test_vertices, uvs=self.test_uvs, cbo=self.test_colors ) str_repr = str(mesh) # The __str__ method returns a formatted string with buffer info self.assertIsInstance(str_repr, str) # Check that it contains some representation of the data self.assertTrue(len(str_repr) > 0)
[docs] def test_to_float32_flat_with_valid_array(self): """Test _to_float32_flat with valid array.""" test_array = [[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]] result = MeshData._to_float32_flat(test_array, "test", required=True) expected = np.array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0], dtype=np.float32) np.testing.assert_array_equal(result, expected)
[docs] def test_to_float32_flat_with_none_required(self): """Test _to_float32_flat with None when required.""" with self.assertRaises(ValueError) as context: MeshData._to_float32_flat(None, "test", required=True) self.assertIn("test is required", str(context.exception))
[docs] def test_to_float32_flat_with_none_not_required(self): """Test _to_float32_flat with None when not required.""" result = MeshData._to_float32_flat(None, "test", required=False) self.assertIsNone(result)
[docs] def test_to_float32_flat_or_none(self): """Test _to_float32_flat_or_none method.""" test_array = [1.0, 2.0, 3.0] result = MeshData._to_float32_flat_or_none(test_array, "test") expected = np.array([1.0, 2.0, 3.0], dtype=np.float32) np.testing.assert_array_equal(result, expected)
[docs] def test_to_int32_flat_with_valid_array(self): """Test _to_int32_flat with valid array.""" test_array = [[1, 2, 3], [4, 5, 6]] result = MeshData._to_int32_flat(test_array, "test", required=True) expected = np.array([1, 2, 3, 4, 5, 6], dtype=np.int32) np.testing.assert_array_equal(result, expected)
[docs] def test_to_int32_flat_with_none_required(self): """Test _to_int32_flat with None when required.""" with self.assertRaises(ValueError) as context: MeshData._to_int32_flat(None, "test", required=True) self.assertIn("test is required", str(context.exception))
[docs] def test_default_colors_for_vertices(self): """Test _default_colors_for_vertices method.""" vertex_count = 3 result = MeshData._default_colors_for_vertices(vertex_count) expected = np.array([1.0, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 0.0], dtype=np.float32) np.testing.assert_array_equal(result, expected)
[docs] def test_default_normals_for_vertices(self): """Test _default_normals_for_vertices method.""" vertex_count = 2 result = MeshData._default_normals_for_vertices(vertex_count) expected = np.array([0.0, 0.0, 1.0, 0.0, 0.0, 1.0], dtype=np.float32) np.testing.assert_array_equal(result, expected)
[docs] def test_from_raw_with_all_parameters(self): """Test from_raw with all parameters provided.""" mesh = MeshData.from_raw( vertices=self.test_vertices, normals=self.test_normals, uvs=self.test_uvs, colors=self.test_colors, indices=self.test_indices ) # Test that data is properly converted and stored self.assertIsNotNone(mesh.vbo) self.assertIsNotNone(mesh.nbo) self.assertIsNotNone(mesh.uvs) self.assertIsNotNone(mesh.cbo) self.assertIsNotNone(mesh.ebo) # Test vertex count expected_vertex_count = len(self.test_vertices) self.assertEqual(mesh.vertex_count, expected_vertex_count)
[docs] def test_from_raw_with_minimal_parameters(self): """Test from_raw with only vertices.""" mesh = MeshData.from_raw(vertices=self.test_vertices) self.assertIsNotNone(mesh.vbo) self.assertIsNotNone(mesh.nbo) # Should be generated self.assertIsNone(mesh.uvs) self.assertIsNotNone(mesh.cbo) # Should be generated self.assertIsNone(mesh.ebo)
[docs] def test_from_raw_with_color_per_vertex_array(self): """Test from_raw with color_per_vertex array.""" color_per_vertex = [0.5, 0.5, 0.5] # Single colour mesh = MeshData.from_raw( vertices=self.test_vertices, color_per_vertex=color_per_vertex ) self.assertIsNotNone(mesh.cbo) # Should have replicated the colour for all vertices expected_length = len(self.test_vertices) * 3 self.assertEqual(len(mesh.cbo), expected_length)
[docs] def test_from_raw_with_color_per_vertex_full_array(self): """Test from_raw with color_per_vertex as full array.""" color_per_vertex = np.array([ [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0], [1.0, 1.0, 0.0] ], dtype=np.float32) mesh = MeshData.from_raw( vertices=self.test_vertices, color_per_vertex=color_per_vertex ) self.assertIsNotNone(mesh.cbo) expected = color_per_vertex.reshape(-1) np.testing.assert_array_equal(mesh.cbo, expected)
[docs] def test_from_raw_with_invalid_color_per_vertex(self): """Test from_raw with invalid color_per_vertex.""" with self.assertRaises(ValueError) as context: MeshData.from_raw( vertices=self.test_vertices, color_per_vertex="invalid" ) self.assertIn("color_per_vertex must be array-like or None", str(context.exception))
[docs] def test_from_raw_with_invalid_uvs_length(self): """Test from_raw with invalid UVs length.""" invalid_uvs = np.array([[0.0, 0.0], [1.0, 1.0]]) # Wrong length with self.assertRaises(ValueError) as context: MeshData.from_raw( vertices=self.test_vertices, uvs=invalid_uvs ) self.assertIn("uvs length must be 2 * vertex_count", str(context.exception))
[docs] def test_bind_with_all_buffers(self): """Test bind method with all buffers present.""" mesh = MeshData( vbo=self.test_vertices, nbo=self.test_normals, uvs=self.test_uvs, cbo=self.test_colors ) mesh.bind()
# Verify OpenGL calls were made # The actual OpenGL calls are mocked in setUp
[docs] def test_bind_with_minimal_buffers(self): """Test bind method with only vertices.""" mesh = MeshData(vbo=self.test_vertices) mesh.bind()
# Should not raise any errors
[docs] def test_bind_with_none_buffers(self): """Test bind method with None buffers.""" mesh = MeshData() mesh.bind()
# Should not raise any errors
[docs] def test_unbind_with_all_buffers(self): """Test unbind method with all buffers present.""" mesh = MeshData( vbo=self.test_vertices, nbo=self.test_normals, uvs=self.test_uvs, cbo=self.test_colors ) mesh.unbind()
# Verify OpenGL calls were made # The actual OpenGL calls are mocked in setUp
[docs] def test_unbind_with_none_buffers(self): """Test unbind method with None buffers.""" mesh = MeshData() mesh.unbind()
# Should not raise any errors
[docs] def test_context_manager(self): """Test that MeshData can be used as a context manager.""" mesh = MeshData(vbo=self.test_vertices) # Test that __enter__ and __exit__ methods exist self.assertTrue(hasattr(mesh, "__enter__")) self.assertTrue(hasattr(mesh, "__exit__")) # Test context manager usage with patch.object(mesh, 'bind') as mock_bind, patch.object(mesh, 'unbind') as mock_unbind: with mesh as context_mesh: # The __enter__ method doesn't return self, it returns None self.assertIsNone(context_mesh) mock_bind.assert_called_once() mock_unbind.assert_called_once()
[docs] def test_draw_with_vertex_colors(self): """Test draw method with vertex colors.""" mesh = MeshData( vbo=self.test_vertices, cbo=self.test_colors, ebo=self.test_indices ) mesh.draw()
# Verify OpenGL calls were made # The actual OpenGL calls are mocked in setUp
[docs] def test_draw_with_override_color(self): """Test draw method with override colour.""" mesh = MeshData( vbo=self.test_vertices, ebo=self.test_indices ) override_color = (1.0, 0.0, 0.0) mesh.draw(color=override_color)
# Verify OpenGL calls were made # The actual OpenGL calls are mocked in setUp
[docs] def test_draw_with_fill_mode(self): """Test draw method with fill mode.""" mesh = MeshData( vbo=self.test_vertices, ebo=self.test_indices ) mesh.draw(fill=True)
# Verify OpenGL calls were made # The actual OpenGL calls are mocked in setUp
[docs] def test_draw_with_alpha_blending(self): """Test draw method with alpha blending.""" mesh = MeshData( vbo=self.test_vertices, ebo=self.test_indices ) mesh.draw(alpha=0.5)
# Verify OpenGL calls were made # The actual OpenGL calls are mocked in setUp
[docs] def test_draw_with_line_width(self): """Test draw method with custom line width.""" mesh = MeshData( vbo=self.test_vertices, ebo=self.test_indices ) mesh.draw(line_width=2.0)
# Verify OpenGL calls were made # The actual OpenGL calls are mocked in setUp
[docs] def test_draw_with_custom_mode(self): """Test draw method with custom drawing mode.""" mesh = MeshData( vbo=self.test_vertices, ebo=self.test_indices ) mesh.draw(mode=GL.GL_LINES)
# Verify OpenGL calls were made # The actual OpenGL calls are mocked in setUp
[docs] def test_draw_without_ebo(self): """Test draw method without EBO.""" mesh = MeshData(vbo=self.test_vertices) # This should raise an error because draw method expects ebo with self.assertRaises(TypeError): mesh.draw()
[docs] def test_vertex_count_calculation(self): """Test vertex count calculation.""" # Test with 2D array vertices_2d = np.array([[1, 2, 3], [4, 5, 6]], dtype=np.float32) mesh = MeshData(vbo=vertices_2d) self.assertEqual(mesh.vertex_count, 2) # Test with 1D array vertices_1d = np.array([1, 2, 3, 4, 5, 6], dtype=np.float32) mesh = MeshData(vbo=vertices_1d) self.assertEqual(mesh.vertex_count, 2)
[docs] def test_data_type_conversion(self): """Test that data is properly converted to correct types.""" # Test with integer vertices int_vertices = np.array([[1, 2, 3], [4, 5, 6]], dtype=np.int32) mesh = MeshData.from_raw(vertices=int_vertices) self.assertEqual(mesh.vbo.dtype, np.float32) # Test with float64 vertices float64_vertices = np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]], dtype=np.float64) mesh = MeshData.from_raw(vertices=float64_vertices) self.assertEqual(mesh.vbo.dtype, np.float32)
[docs] def test_array_flattening(self): """Test that multi-dimensional arrays are properly flattened.""" # Test with 3D array vertices_3d = np.array([[[1, 2, 3], [4, 5, 6]]], dtype=np.float32) mesh = MeshData.from_raw(vertices=vertices_3d) self.assertEqual(mesh.vbo.ndim, 1) self.assertEqual(len(mesh.vbo), 6)
[docs] def test_error_handling_in_from_raw(self): """Test error handling in from_raw method.""" # Test with None vertices with self.assertRaises(ValueError) as context: MeshData.from_raw(vertices=None) self.assertIn("vertices is required", str(context.exception))
[docs] def test_color_generation_edge_cases(self): """Test colour generation with edge cases.""" # Test with single vertex single_vertex = np.array([[1, 2, 3]], dtype=np.float32) mesh = MeshData.from_raw(vertices=single_vertex) self.assertIsNotNone(mesh.cbo) self.assertEqual(len(mesh.cbo), 3) # Single colour (RGB) # Test with empty vertex array empty_vertices = np.array([], dtype=np.float32).reshape(0, 3) mesh = MeshData.from_raw(vertices=empty_vertices) self.assertIsNotNone(mesh.cbo) self.assertEqual(len(mesh.cbo), 0)
[docs] def test_normal_generation(self): """Test normal generation when not provided.""" mesh = MeshData.from_raw(vertices=self.test_vertices) self.assertIsNotNone(mesh.nbo) # Should have generated normals for all vertices expected_length = len(self.test_vertices) * 3 self.assertEqual(len(mesh.nbo), expected_length)
[docs] def test_repr_string(self): """Test string representation of MeshData.""" mesh = MeshData( vbo=self.test_vertices, uvs=self.test_uvs, cbo=self.test_colors ) repr_str = str(mesh) # The __str__ method returns a formatted string with buffer info self.assertIsInstance(repr_str, str)
if __name__ == "__main__": unittest.main()