"""
Unit tests for the TextureRenderer 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.texture.TextureRenderer`
class, which provides specialized texture rendering functionality.
The tests cover:
- Object initialization and inheritance from ObjectRenderer
- Texture initialization and loading
- Path management and resolution
- UV coordinate handling and inversion
- 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.texture.TextureRenderer
- picogl.renderer.meshdata.MeshData
To run the tests::
python -m unittest picogl.tests.test_texture_renderer
"""
import unittest
from unittest.mock import MagicMock, patch, call
from pathlib import Path
import numpy as np
from picogl.renderer.texture import TextureRenderer
from picogl.renderer.meshdata import MeshData
[docs]
class TestTextureRenderer(unittest.TestCase):
"""Test cases for TextureRenderer 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.texture.GLContext'),
patch('picogl.renderer.texture.MeshData'),
patch('picogl.renderer.texture.ObjectRenderer'),
patch('picogl.renderer.texture.TextureLoader'),
]
# 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_default_parameters(self):
"""Test TextureRenderer initialization with default parameters."""
mock_context = MagicMock()
with patch('picogl.renderer.texture.TextureLoader') as mock_texture_loader:
mock_texture = MagicMock()
mock_texture.texture_gl_id = 123
mock_texture.inversed_v_coords = False
mock_texture_loader.return_value = mock_texture
renderer = TextureRenderer(
context=mock_context,
data=self.test_mesh_data
)
# Test basic properties
self.assertEqual(renderer.context, mock_context)
self.assertEqual(renderer.data, self.test_mesh_data)
self.assertTrue(renderer.show_model)
self.assertIsNone(renderer.base_dir)
self.assertIsNone(renderer.glsl_dir)
# resource_subdir is set to "tu02" by default in get_texture_filename
self.assertEqual(renderer.resource_subdir, "tu02")
# Test texture-related properties
self.assertIsNotNone(renderer.texture_full_path)
self.assertIsNotNone(renderer.resource_full_path)
self.assertEqual(renderer.texture, mock_texture)
self.assertEqual(renderer.context.texture_id, 123)
[docs]
def test_initialization_with_custom_parameters(self):
"""Test TextureRenderer initialization with custom parameters."""
mock_context = MagicMock()
base_dir = "/test/path"
glsl_dir = "/test/glsl"
with patch('picogl.renderer.texture.TextureLoader') as mock_texture_loader:
mock_texture = MagicMock()
mock_texture.texture_gl_id = 456
mock_texture.inversed_v_coords = False
mock_texture_loader.return_value = mock_texture
renderer = TextureRenderer(
context=mock_context,
data=self.test_mesh_data,
base_dir=base_dir,
glsl_dir=glsl_dir,
use_texture=True,
texture_file="custom.tga",
resource_subdir="tu03"
)
# Test basic properties
self.assertEqual(renderer.base_dir, base_dir)
self.assertEqual(renderer.glsl_dir, glsl_dir)
self.assertEqual(renderer.base_path, Path(base_dir))
# Test texture properties
self.assertEqual(renderer.texture, mock_texture)
self.assertEqual(renderer.context.texture_id, 456)
[docs]
def test_initialization_with_path_objects(self):
"""Test TextureRenderer initialization with Path objects."""
mock_context = MagicMock()
base_path = Path("/test/path")
glsl_path = Path("/test/glsl")
with patch('picogl.renderer.texture.TextureLoader') as mock_texture_loader:
mock_texture = MagicMock()
mock_texture.texture_gl_id = 789
mock_texture.inversed_v_coords = False
mock_texture_loader.return_value = mock_texture
renderer = TextureRenderer(
context=mock_context,
data=self.test_mesh_data,
base_dir=base_path,
glsl_dir=glsl_path
)
# Test path handling
self.assertEqual(renderer.base_dir, base_path)
self.assertEqual(renderer.glsl_dir, glsl_path)
self.assertEqual(renderer.base_path, base_path)
[docs]
def test_initialization_with_none_base_dir(self):
"""Test TextureRenderer initialization with None base_dir."""
mock_context = MagicMock()
with patch('picogl.renderer.texture.TextureLoader') as mock_texture_loader:
mock_texture = MagicMock()
mock_texture.texture_gl_id = 111
mock_texture.inversed_v_coords = False
mock_texture_loader.return_value = mock_texture
renderer = TextureRenderer(
context=mock_context,
data=self.test_mesh_data,
base_dir=None
)
# Test that base_path is still created
self.assertIsNone(renderer.base_dir)
self.assertIsNotNone(renderer.base_path)
[docs]
def test_initialize_textures_with_normal_v_coords(self):
"""Test initialize_textures with normal V coordinates."""
mock_context = MagicMock()
original_uvs = self.test_uvs.copy()
with patch('picogl.renderer.texture.TextureLoader') as mock_texture_loader:
mock_texture = MagicMock()
mock_texture.texture_gl_id = 222
mock_texture.inversed_v_coords = False
mock_texture_loader.return_value = mock_texture
renderer = TextureRenderer(
context=mock_context,
data=self.test_mesh_data
)
# Test that UVs are not modified (compare flattened arrays)
np.testing.assert_array_equal(renderer.data.uvs.flatten(), original_uvs.flatten())
# Test texture setup
self.assertEqual(renderer.texture, mock_texture)
self.assertEqual(renderer.context.texture_id, 222)
[docs]
def test_initialize_textures_with_inversed_v_coords(self):
"""Test initialize_textures with inversed V coordinates."""
mock_context = MagicMock()
original_uvs = self.test_uvs.copy()
with patch('picogl.renderer.texture.TextureLoader') as mock_texture_loader:
mock_texture = MagicMock()
mock_texture.texture_gl_id = 333
mock_texture.inversed_v_coords = True
mock_texture_loader.return_value = mock_texture
renderer = TextureRenderer(
context=mock_context,
data=self.test_mesh_data
)
# Test that UVs are modified (V coordinates inverted)
# The inversion works on the flattened array, so we need to work with flattened data
expected_uvs_flat = original_uvs.flatten().copy()
for index, _ in enumerate(expected_uvs_flat):
if index % 2: # Odd indices are V coordinates
expected_uvs_flat[index] = 1.0 - expected_uvs_flat[index]
np.testing.assert_array_equal(renderer.data.uvs.flatten(), expected_uvs_flat)
# Test texture setup
self.assertEqual(renderer.texture, mock_texture)
self.assertEqual(renderer.context.texture_id, 333)
[docs]
def test_get_texture_filename(self):
"""Test get_texture_filename method."""
mock_context = MagicMock()
with patch('picogl.renderer.texture.TextureLoader') as mock_texture_loader:
mock_texture = MagicMock()
mock_texture.texture_gl_id = 444
mock_texture.inversed_v_coords = False
mock_texture_loader.return_value = mock_texture
renderer = TextureRenderer(
context=mock_context,
data=self.test_mesh_data,
base_dir="/test/path"
)
# Test that texture filename is set correctly
expected_path = str(Path("/test/path").absolute() / "resources" / "tu02" / "uvtemplate.tga")
self.assertEqual(str(renderer.texture_full_path), expected_path)
# Test that resource path is set
expected_resource_path = Path("/test/path").absolute() / "resources" / "tu02"
self.assertEqual(renderer.resource_full_path, expected_resource_path)
[docs]
def test_set_texture_filename(self):
"""Test set_texture_filename method."""
mock_context = MagicMock()
with patch('picogl.renderer.texture.TextureLoader') as mock_texture_loader:
mock_texture = MagicMock()
mock_texture.texture_gl_id = 555
mock_texture.inversed_v_coords = False
mock_texture_loader.return_value = mock_texture
renderer = TextureRenderer(
context=mock_context,
data=self.test_mesh_data,
base_dir="/test/path"
)
# Test setting custom texture filename
renderer.set_texture_filename("custom_texture.png")
expected_path = Path("/test/path").absolute() / "resources" / "tu02" / "custom_texture.png"
self.assertEqual(renderer.texture_full_path, expected_path)
[docs]
def test_set_texture_filename_with_none(self):
"""Test set_texture_filename method with None filename."""
mock_context = MagicMock()
with patch('picogl.renderer.texture.TextureLoader') as mock_texture_loader:
mock_texture = MagicMock()
mock_texture.texture_gl_id = 666
mock_texture.inversed_v_coords = False
mock_texture_loader.return_value = mock_texture
renderer = TextureRenderer(
context=mock_context,
data=self.test_mesh_data,
base_dir="/test/path"
)
# Test setting None texture filename
renderer.set_texture_filename(None)
expected_path = Path("/test/path").absolute() / "resources" / "tu02" / "default_texture.tga"
self.assertEqual(renderer.texture_full_path, expected_path)
[docs]
def test_set_resource_path(self):
"""Test set_resource_path method."""
mock_context = MagicMock()
with patch('picogl.renderer.texture.TextureLoader') as mock_texture_loader:
mock_texture = MagicMock()
mock_texture.texture_gl_id = 777
mock_texture.inversed_v_coords = False
mock_texture_loader.return_value = mock_texture
renderer = TextureRenderer(
context=mock_context,
data=self.test_mesh_data,
base_dir="/test/path"
)
# Test setting custom resource path
renderer.set_resource_path("/custom/base", "custom_subdir")
expected_path = Path("/custom/base").absolute() / "resources" / "custom_subdir"
self.assertEqual(renderer.resource_full_path, expected_path)
[docs]
def test_set_resource_path_with_path_object(self):
"""Test set_resource_path method with Path object."""
mock_context = MagicMock()
with patch('picogl.renderer.texture.TextureLoader') as mock_texture_loader:
mock_texture = MagicMock()
mock_texture.texture_gl_id = 888
mock_texture.inversed_v_coords = False
mock_texture_loader.return_value = mock_texture
renderer = TextureRenderer(
context=mock_context,
data=self.test_mesh_data,
base_dir="/test/path"
)
# Test setting resource path with Path object
custom_path = Path("/custom/base")
renderer.set_resource_path(custom_path, "custom_subdir")
expected_path = custom_path.absolute() / "resources" / "custom_subdir"
self.assertEqual(renderer.resource_full_path, expected_path)
[docs]
def test_object_renderer_inheritance(self):
"""Test that TextureRenderer inherits from ObjectRenderer."""
mock_context = MagicMock()
with patch('picogl.renderer.texture.TextureLoader') as mock_texture_loader:
mock_texture = MagicMock()
mock_texture.texture_gl_id = 999
mock_texture.inversed_v_coords = False
mock_texture_loader.return_value = mock_texture
renderer = TextureRenderer(
context=mock_context,
data=self.test_mesh_data
)
# Test inheritance
from picogl.renderer.object import ObjectRenderer
self.assertIsInstance(renderer, ObjectRenderer)
# Test that ObjectRenderer methods are available
self.assertTrue(hasattr(renderer, 'initialize_shaders'))
self.assertTrue(hasattr(renderer, 'initialize'))
self.assertTrue(hasattr(renderer, 'render'))
self.assertTrue(hasattr(renderer, '_draw_model'))
[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)
with patch('picogl.renderer.texture.TextureLoader') as mock_texture_loader:
mock_texture = MagicMock()
mock_texture.texture_gl_id = 1000
mock_texture.inversed_v_coords = False
mock_texture_loader.return_value = mock_texture
renderer = TextureRenderer(
context=mock_context,
data=mesh_data_2d
)
expected_vertex_count = len(vertices_2d.flatten()) // 3
self.assertEqual(renderer.data.vertex_count, expected_vertex_count)
[docs]
def test_texture_loading_integration(self):
"""Test texture loading integration with TextureLoader."""
mock_context = MagicMock()
with patch('picogl.renderer.texture.TextureLoader') as mock_texture_loader:
mock_texture = MagicMock()
mock_texture.texture_gl_id = 1111
mock_texture.inversed_v_coords = True
mock_texture_loader.return_value = mock_texture
renderer = TextureRenderer(
context=mock_context,
data=self.test_mesh_data,
base_dir="/test/path"
)
# Test that TextureLoader was called with correct path
expected_path = str(Path("/test/path").absolute() / "resources" / "tu02" / "uvtemplate.tga")
mock_texture_loader.assert_called_once_with(expected_path)
# Test texture assignment
self.assertEqual(renderer.texture, mock_texture)
self.assertEqual(renderer.context.texture_id, 1111)
[docs]
def test_uv_coordinate_inversion_edge_cases(self):
"""Test UV coordinate inversion with edge cases."""
mock_context = MagicMock()
# Test with UVs that have specific patterns
test_uvs = np.array([
[0.0, 0.0], # index 0: U=0, V=0
[1.0, 0.5], # index 1: U=1, V=0.5
[0.5, 1.0], # index 2: U=0.5, V=1
[1.0, 1.0] # index 3: U=1, V=1
], dtype=np.float32)
# Create a copy to avoid modification during test
test_uvs_copy = test_uvs.copy()
mesh_data_custom_uvs = MeshData.from_raw(
vertices=self.test_vertices,
uvs=test_uvs_copy
)
with patch('picogl.renderer.texture.TextureLoader') as mock_texture_loader:
mock_texture = MagicMock()
mock_texture.texture_gl_id = 2222
mock_texture.inversed_v_coords = True
mock_texture_loader.return_value = mock_texture
renderer = TextureRenderer(
context=mock_context,
data=mesh_data_custom_uvs
)
# Test that only V coordinates (odd indices) are inverted
# The inversion works on the flattened array, so we need to work with flattened data
expected_uvs_flat = test_uvs.flatten().copy()
for index, _ in enumerate(expected_uvs_flat):
if index % 2: # Odd indices are V coordinates
expected_uvs_flat[index] = 1.0 - expected_uvs_flat[index]
# Expected result: [0. 1. 1. 0.5 0.5 0. 1. 0. ]
# This matches the actual behavior of the TextureRenderer
np.testing.assert_array_equal(renderer.data.uvs.flatten(), expected_uvs_flat)
[docs]
def test_draw_selection_method(self):
"""Test _draw_selection method exists and is callable."""
mock_context = MagicMock()
with patch('picogl.renderer.texture.TextureLoader') as mock_texture_loader:
mock_texture = MagicMock()
mock_texture.texture_gl_id = 3333
mock_texture.inversed_v_coords = False
mock_texture_loader.return_value = mock_texture
renderer = TextureRenderer(
context=mock_context,
data=self.test_mesh_data
)
# Test that _draw_selection method exists and is callable
self.assertTrue(hasattr(renderer, '_draw_selection'))
self.assertTrue(callable(getattr(renderer, '_draw_selection')))
# Test that it can be called without errors
renderer._draw_selection() # Should not raise an exception
[docs]
def test_initialization_with_minimal_mesh_data(self):
"""Test initialization with minimal mesh data."""
minimal_mesh_data = MeshData.from_raw(vertices=self.test_vertices)
mock_context = MagicMock()
with patch('picogl.renderer.texture.TextureLoader') as mock_texture_loader:
mock_texture = MagicMock()
mock_texture.texture_gl_id = 4444
mock_texture.inversed_v_coords = False
mock_texture_loader.return_value = mock_texture
renderer = TextureRenderer(
context=mock_context,
data=minimal_mesh_data
)
# Test that initialization succeeds with minimal data
self.assertEqual(renderer.data, minimal_mesh_data)
self.assertEqual(renderer.texture, mock_texture)
self.assertEqual(renderer.context.texture_id, 4444)
[docs]
def test_path_absolute_conversion(self):
"""Test that paths are converted to absolute paths."""
mock_context = MagicMock()
relative_path = "relative/path"
with patch('picogl.renderer.texture.TextureLoader') as mock_texture_loader:
mock_texture = MagicMock()
mock_texture.texture_gl_id = 5555
mock_texture.inversed_v_coords = False
mock_texture_loader.return_value = mock_texture
renderer = TextureRenderer(
context=mock_context,
data=self.test_mesh_data,
base_dir=relative_path
)
# Test that base_path is created from relative path
self.assertEqual(renderer.base_path, Path(relative_path))
# Test that resource_full_path is absolute
self.assertTrue(renderer.resource_full_path.is_absolute())
[docs]
def test_texture_gl_id_assignment(self):
"""Test that texture_gl_id is properly assigned to context."""
mock_context = MagicMock()
with patch('picogl.renderer.texture.TextureLoader') as mock_texture_loader:
mock_texture = MagicMock()
mock_texture.texture_gl_id = 6666
mock_texture.inversed_v_coords = False
mock_texture_loader.return_value = mock_texture
renderer = TextureRenderer(
context=mock_context,
data=self.test_mesh_data
)
# Test that texture_gl_id is assigned to context
self.assertEqual(mock_context.texture_id, 6666)
# Test that texture is stored in renderer
self.assertEqual(renderer.texture, mock_texture)
[docs]
def test_initialization_calls_initialize_textures(self):
"""Test that initialization calls initialize_textures."""
mock_context = MagicMock()
with patch('picogl.renderer.texture.TextureLoader') as mock_texture_loader:
mock_texture = MagicMock()
mock_texture.texture_gl_id = 7777
mock_texture.inversed_v_coords = False
mock_texture_loader.return_value = mock_texture
with patch.object(TextureRenderer, 'initialize_textures') as mock_init_textures:
renderer = TextureRenderer(
context=mock_context,
data=self.test_mesh_data
)
# Test that initialize_textures was called
mock_init_textures.assert_called_once()
if __name__ == "__main__":
unittest.main()