Source code for picogl.tests.test_object_renderer

"""
Unit tests for the ObjectRenderer 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.object.ObjectRenderer`
class, which provides unified rendering for textured and untextured objects.

The tests cover:

- Object initialization with various parameters
- Shader initialization and compilation
- VAO and VBO setup for different data types
- Rendering pipeline and drawing operations
- Texture loading and binding
- 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.object.ObjectRenderer
    - picogl.renderer.meshdata.MeshData

To run the tests::

    python -m unittest picogl.tests.test_object_renderer

"""

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

import numpy as np
from OpenGL.raw.GL.VERSION.GL_1_0 import GL_TRIANGLES

from picogl.renderer.object import ObjectRenderer
from picogl.renderer.meshdata import MeshData


[docs] class TestObjectRenderer(unittest.TestCase): """Test cases for ObjectRenderer 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_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_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_uvs = np.array([ [0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0] ], dtype=np.float32) # Create test MeshData self.test_mesh_data = MeshData.from_raw( vertices=self.test_vertices, colors=self.test_colors, normals=self.test_normals, uvs=self.test_uvs ) # Mock OpenGL functions and dependencies self.gl_patches = [ patch('picogl.renderer.object.GLContext'), patch('picogl.renderer.object.VertexArrayObject'), patch('picogl.renderer.object.bind_texture_array'), patch('picogl.renderer.object.TextureLoader'), patch('picogl.renderer.object.log'), ] # 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_without_texture(self): """Test ObjectRenderer initialization without texture.""" mock_context = MagicMock() renderer = ObjectRenderer( context=mock_context, data=self.test_mesh_data, use_texture=False ) # Test basic properties self.assertEqual(renderer.context, mock_context) self.assertEqual(renderer.data, self.test_mesh_data) self.assertFalse(renderer.use_texture) self.assertIsNone(renderer.texture) self.assertTrue(renderer.show_model) self.assertIsNone(renderer.base_dir) self.assertIsNone(renderer.glsl_dir) self.assertEqual(renderer.resource_subdir, "tu02") # Test vertex count calculation expected_vertex_count = len(self.test_vertices.flatten()) // 3 self.assertEqual(renderer.data.vertex_count, expected_vertex_count)
[docs] def test_initialization_with_texture(self): """Test ObjectRenderer initialization with texture.""" mock_context = MagicMock() mock_texture_loader = MagicMock() mock_texture_loader.texture_glid = 123 with patch('picogl.renderer.object.TextureLoader', return_value=mock_texture_loader): renderer = ObjectRenderer( context=mock_context, data=self.test_mesh_data, base_dir="/test/path", use_texture=True, texture_file="test_texture.tga", resource_subdir="tu03" ) # Test basic properties self.assertEqual(renderer.context, mock_context) self.assertEqual(renderer.data, self.test_mesh_data) self.assertTrue(renderer.use_texture) self.assertEqual(renderer.texture, mock_texture_loader) self.assertEqual(renderer.base_dir, "/test/path") self.assertEqual(renderer.resource_subdir, "tu03") self.assertEqual(renderer.texture_file, "test_texture.tga") # Test texture ID assignment self.assertEqual(renderer.context.texture_id, 123)
[docs] def test_initialization_with_path_objects(self): """Test ObjectRenderer initialization with Path objects.""" mock_context = MagicMock() base_path = Path("/test/path") glsl_path = Path("/test/glsl") renderer = ObjectRenderer( context=mock_context, data=self.test_mesh_data, base_dir=base_path, glsl_dir=glsl_path, use_texture=False ) self.assertEqual(renderer.base_dir, base_path) self.assertEqual(renderer.glsl_dir, glsl_path)
[docs] def test_initialization_with_none_context(self): """Test ObjectRenderer initialization with None context.""" renderer = ObjectRenderer( context=None, data=self.test_mesh_data, use_texture=False ) self.assertIsNone(renderer.context)
[docs] def test_initialize_shaders_with_existing_context(self): """Test initialize_shaders with existing context.""" mock_context = MagicMock() renderer = ObjectRenderer( context=mock_context, data=self.test_mesh_data, use_texture=False ) renderer.initialize_shaders() # Test shader program creation mock_context.create_shader_program.assert_called_once_with( vertex_source_file="vertex.glsl", fragment_source_file="fragment.glsl", glsl_dir=None )
[docs] def test_initialize_shaders_with_custom_glsl_dir(self): """Test initialize_shaders with custom GLSL directory.""" mock_context = MagicMock() renderer = ObjectRenderer( context=mock_context, data=self.test_mesh_data, glsl_dir="/custom/glsl", use_texture=False ) renderer.initialize_shaders() # Test shader program creation with custom directory mock_context.create_shader_program.assert_called_once_with( vertex_source_file="vertex.glsl", fragment_source_file="fragment.glsl", glsl_dir="/custom/glsl" )
[docs] def test_initialize_shaders_with_none_context(self): """Test initialize_shaders with None context creates new one.""" with patch('picogl.renderer.object.GLContext') as mock_gl_context_class: mock_new_context = MagicMock() mock_gl_context_class.return_value = mock_new_context renderer = ObjectRenderer( context=None, data=self.test_mesh_data, use_texture=False ) renderer.initialize_shaders() # Test new context creation mock_gl_context_class.assert_called_once() self.assertEqual(renderer.context, mock_new_context) # Test shader program creation mock_new_context.create_shader_program.assert_called_once()
[docs] def test_initialize_with_texture_and_uvs(self): """Test initialize method with texture and UVs.""" mock_context = MagicMock() mock_context.vaos = {} mock_vao = MagicMock() with patch('picogl.renderer.object.VertexArrayObject', return_value=mock_vao): renderer = ObjectRenderer( context=mock_context, data=self.test_mesh_data, use_texture=True ) renderer.initialize() # Test VAO creation and VBO setup mock_vao.add_vbo.assert_called() # Test VAO assignment self.assertEqual(mock_context.vaos["model"], mock_vao) # Test VBO calls for texture mode calls = mock_vao.add_vbo.call_args_list self.assertEqual(len(calls), 2) # vertices + UVs # Test vertex VBO (index 0, size 3) vertex_call = next(call for call in calls if call[1].get('index') == 0) self.assertEqual(vertex_call[1]['index'], 0) self.assertEqual(vertex_call[1]['size'], 3) np.testing.assert_array_equal(vertex_call[1]['data'], self.test_mesh_data.vbo) # Test UV VBO (index 1, size 2) uv_call = next(call for call in calls if call[1].get('index') == 1) self.assertEqual(uv_call[1]['index'], 1) self.assertEqual(uv_call[1]['size'], 2) np.testing.assert_array_equal(uv_call[1]['data'], self.test_mesh_data.uvs)
[docs] def test_initialize_without_texture_with_colors_and_normals(self): """Test initialize method without texture, with colors and normals.""" mock_context = MagicMock() mock_context.vaos = {} mock_vao = MagicMock() with patch('picogl.renderer.object.VertexArrayObject', return_value=mock_vao): renderer = ObjectRenderer( context=mock_context, data=self.test_mesh_data, use_texture=False ) renderer.initialize() # Test VAO creation and VBO setup mock_vao.add_vbo.assert_called() # Test VAO assignment self.assertEqual(mock_context.vaos["model"], mock_vao) # Test VBO calls for colour/normal mode calls = mock_vao.add_vbo.call_args_list self.assertEqual(len(calls), 3) # vertices + colors + normals # Test vertex VBO (index 0, size 3) vertex_call = next(call for call in calls if call[1].get('index') == 0) self.assertEqual(vertex_call[1]['index'], 0) self.assertEqual(vertex_call[1]['size'], 3) # Test colour VBO (index 1, size 3) color_call = next(call for call in calls if call[1].get('index') == 1) self.assertEqual(color_call[1]['index'], 1) self.assertEqual(color_call[1]['size'], 3) np.testing.assert_array_equal(color_call[1]['data'], self.test_mesh_data.cbo) # Test normal VBO (index 2, size 3) normal_call = next(call for call in calls if call[1].get('index') == 2) self.assertEqual(normal_call[1]['index'], 2) self.assertEqual(normal_call[1]['size'], 3) np.testing.assert_array_equal(normal_call[1]['data'], self.test_mesh_data.nbo)
[docs] def test_initialize_without_texture_without_colors(self): """Test initialize method without texture and without colors.""" # Create mesh data without colors mesh_data_no_colors = MeshData.from_raw( vertices=self.test_vertices, normals=self.test_normals, uvs=self.test_uvs ) mock_context = MagicMock() mock_context.vaos = {} mock_vao = MagicMock() with patch('picogl.renderer.object.VertexArrayObject', return_value=mock_vao): renderer = ObjectRenderer( context=mock_context, data=mesh_data_no_colors, use_texture=False ) renderer.initialize() # Test VBO calls - ObjectRenderer always adds colors and normals when not using texture calls = mock_vao.add_vbo.call_args_list self.assertEqual(len(calls), 3) # vertices + colors + normals # Test that colors VBO was added (ObjectRenderer behavior) color_calls = [call for call in calls if call[1].get('index') == 1 and call[1].get('size') == 3] self.assertEqual(len(color_calls), 1)
[docs] def test_initialize_with_existing_vaos(self): """Test initialize method with existing VAOs dictionary.""" mock_context = MagicMock() mock_context.vaos = {"existing": "vao"} mock_vao = MagicMock() with patch('picogl.renderer.object.VertexArrayObject', return_value=mock_vao): renderer = ObjectRenderer( context=mock_context, data=self.test_mesh_data, use_texture=False ) renderer.initialize() # Test that existing VAOs are preserved self.assertIn("existing", mock_context.vaos) self.assertIn("model", mock_context.vaos)
[docs] def test_render_with_show_model_true(self): """Test render method with show_model=True.""" mock_context = MagicMock() mock_vao = MagicMock() mock_shader = MagicMock() mock_context.vaos = {"model": mock_vao} mock_context.shader = mock_shader renderer = ObjectRenderer( context=mock_context, data=self.test_mesh_data, use_texture=False ) with patch.object(renderer, '_draw_model') as mock_draw_model: with patch.object(renderer, '_finalize_render') as mock_finalize: renderer.render() mock_draw_model.assert_called_once() mock_finalize.assert_called_once()
[docs] def test_render_with_show_model_false(self): """Test render method with show_model=False.""" mock_context = MagicMock() renderer = ObjectRenderer( context=mock_context, data=self.test_mesh_data, use_texture=False ) renderer.show_model = False with patch.object(renderer, '_draw_model') as mock_draw_model: with patch.object(renderer, '_finalize_render') as mock_finalize: renderer.render() mock_draw_model.assert_not_called() mock_finalize.assert_called_once()
[docs] def test_draw_model_without_texture(self): """Test _draw_model method without texture.""" mock_context = MagicMock() mock_vao = MagicMock() mock_shader = MagicMock() mock_context.vaos = {"model": mock_vao} mock_context.shader = mock_shader mock_context.mvp_matrix = np.eye(4) mock_context.model_matrix = np.eye(4) mock_context.eye_np = np.array([0, 0, 5]) renderer = ObjectRenderer( context=mock_context, data=self.test_mesh_data, use_texture=False ) with patch.object(mock_vao, 'draw') as mock_draw: renderer._draw_model() # Test shader context manager usage mock_shader.__enter__.assert_called_once() mock_shader.__exit__.assert_called_once() # Test VAO context manager usage mock_vao.__enter__.assert_called_once() mock_vao.__exit__.assert_called_once() # Test uniform setting mock_shader.uniform.assert_any_call("mvp_matrix", mock_context.mvp_matrix) mock_shader.uniform.assert_any_call("model_matrix", mock_context.model_matrix) mock_shader.uniform.assert_any_call("viewPos", mock_context.eye_np) # Test draw call mock_draw.assert_called_once_with(mode=GL_TRIANGLES, index_count=renderer.data.vertex_count)
[docs] def test_draw_model_with_texture(self): """Test _draw_model method with texture.""" mock_context = MagicMock() mock_vao = MagicMock() mock_shader = MagicMock() mock_texture = MagicMock() mock_context.vaos = {"model": mock_vao} mock_context.shader = mock_shader mock_context.mvp_matrix = np.eye(4) mock_context.model_matrix = np.eye(4) mock_context.eye_np = np.array([0, 0, 5]) mock_context.texture_id = 123 renderer = ObjectRenderer( context=mock_context, data=self.test_mesh_data, use_texture=True ) renderer.texture = mock_texture with patch.object(mock_vao, 'draw') as mock_draw: with patch('picogl.renderer.object.bind_texture_array') as mock_bind_texture: renderer._draw_model() # Test texture binding mock_bind_texture.assert_called_once_with(123) # Test texture uniform mock_shader.uniform.assert_any_call("texture0", 0)
[docs] def test_draw_model_without_texture_object(self): """Test _draw_model method with use_texture=True but no texture object.""" mock_context = MagicMock() mock_vao = MagicMock() mock_shader = MagicMock() mock_context.vaos = {"model": mock_vao} mock_context.shader = mock_shader mock_context.mvp_matrix = np.eye(4) mock_context.model_matrix = np.eye(4) mock_context.eye_np = np.array([0, 0, 5]) renderer = ObjectRenderer( context=mock_context, data=self.test_mesh_data, use_texture=True ) renderer.texture = None # No texture object with patch.object(mock_vao, 'draw') as mock_draw: with patch('picogl.renderer.object.bind_texture_array') as mock_bind_texture: renderer._draw_model() # Test that texture binding was not called mock_bind_texture.assert_not_called() # Test that texture uniform was not set texture_uniform_calls = [call for call in mock_shader.uniform.call_args_list if call[0][0] == "texture0"] self.assertEqual(len(texture_uniform_calls), 0)
[docs] def test_vertex_count_calculation(self): """Test vertex count calculation in initialization.""" mock_context = MagicMock() # Test with different vertex data vertices_2d = np.array([[0, 0, 0], [1, 1, 1]], dtype=np.float32) mesh_data_2d = MeshData.from_raw(vertices=vertices_2d) renderer = ObjectRenderer( context=mock_context, data=mesh_data_2d, use_texture=False ) expected_vertex_count = len(vertices_2d.flatten()) // 3 self.assertEqual(renderer.data.vertex_count, expected_vertex_count)
[docs] def test_texture_loading_with_valid_path(self): """Test texture loading with valid path.""" mock_context = MagicMock() mock_texture_loader = MagicMock() mock_texture_loader.texture_glid = 456 with patch('picogl.renderer.object.TextureLoader', return_value=mock_texture_loader): renderer = ObjectRenderer( context=mock_context, data=self.test_mesh_data, base_dir="/test/path", use_texture=True, texture_file="test.tga", resource_subdir="tu04" ) # Test texture loader creation self.assertEqual(renderer.texture, mock_texture_loader) # Test texture ID assignment self.assertEqual(renderer.context.texture_id, 456)
[docs] def test_texture_loading_without_texture_file(self): """Test texture loading without texture file.""" mock_context = MagicMock() renderer = ObjectRenderer( context=mock_context, data=self.test_mesh_data, base_dir="/test/path", use_texture=True, texture_file=None ) # Should not create texture loader self.assertIsNone(renderer.texture)
[docs] def test_texture_loading_without_base_dir(self): """Test texture loading without base directory.""" mock_context = MagicMock() renderer = ObjectRenderer( context=mock_context, data=self.test_mesh_data, base_dir=None, use_texture=True, texture_file="test.tga" ) # Should use current directory as base self.assertIsNotNone(renderer.texture)
[docs] def test_renderer_base_inheritance(self): """Test that ObjectRenderer inherits from RendererBase.""" mock_context = MagicMock() renderer = ObjectRenderer( context=mock_context, data=self.test_mesh_data, use_texture=False ) # Test inheritance from picogl.renderer import RendererBase self.assertIsInstance(renderer, RendererBase)
[docs] def test_initialization_with_minimal_mesh_data(self): """Test initialization with minimal mesh data (only vertices).""" minimal_mesh_data = MeshData.from_raw(vertices=self.test_vertices) mock_context = MagicMock() mock_context.vaos = {} mock_vao = MagicMock() with patch('picogl.renderer.object.VertexArrayObject', return_value=mock_vao): renderer = ObjectRenderer( context=mock_context, data=minimal_mesh_data, use_texture=False ) renderer.initialize() # Should have vertex VBO + colors + normals (ObjectRenderer always adds them) calls = mock_vao.add_vbo.call_args_list self.assertEqual(len(calls), 3) # vertices + colors + normals # Test vertex VBO vertex_call = calls[0] self.assertEqual(vertex_call[1]['index'], 0) self.assertEqual(vertex_call[1]['size'], 3)
[docs] def test_initialization_with_texture_but_no_uvs(self): """Test initialization with texture enabled but no UVs in mesh data.""" mesh_data_no_uvs = MeshData.from_raw( vertices=self.test_vertices, colors=self.test_colors, normals=self.test_normals ) mock_context = MagicMock() mock_context.vaos = {} mock_vao = MagicMock() with patch('picogl.renderer.object.VertexArrayObject', return_value=mock_vao): renderer = ObjectRenderer( context=mock_context, data=mesh_data_no_uvs, use_texture=True ) renderer.initialize() # Should fall back to colour/normal mode calls = mock_vao.add_vbo.call_args_list self.assertEqual(len(calls), 3) # vertices + colors + normals # Test that UV VBO was not added uv_calls = [call for call in calls if call[1].get('index') == 1 and call[1].get('size') == 2] self.assertEqual(len(uv_calls), 0)
[docs] def test_log_parameter_calls(self): """Test that log.parameter is called for texture usage.""" mock_context = MagicMock() with patch('picogl.renderer.object.log') as mock_log: ObjectRenderer( context=mock_context, data=self.test_mesh_data, use_texture=True ) # Test log parameter call mock_log.parameter.assert_called_once_with("Using texture", True)
[docs] def test_finalize_render_inheritance(self): """Test that _finalize_render is inherited from RendererBase.""" mock_context = MagicMock() renderer = ObjectRenderer( context=mock_context, data=self.test_mesh_data, use_texture=False ) # Test that _finalize_render method exists self.assertTrue(hasattr(renderer, '_finalize_render')) self.assertTrue(callable(getattr(renderer, '_finalize_render')))
if __name__ == "__main__": unittest.main()