| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326 |
- """Unit tests for the STL thumbnail service."""
- import os
- import tempfile
- from pathlib import Path
- from unittest.mock import MagicMock, patch
- import pytest
- class TestSTLThumbnailService:
- """Tests for STL thumbnail generation."""
- def test_generate_thumbnail_ascii_stl(self):
- """Test generating thumbnail from ASCII STL file."""
- from backend.app.services.stl_thumbnail import generate_stl_thumbnail
- # Create a simple ASCII STL cube
- ascii_stl = """solid cube
- facet normal 0 0 -1
- outer loop
- vertex 0 0 0
- vertex 1 0 0
- vertex 1 1 0
- endloop
- endfacet
- facet normal 0 0 -1
- outer loop
- vertex 0 0 0
- vertex 1 1 0
- vertex 0 1 0
- endloop
- endfacet
- facet normal 0 0 1
- outer loop
- vertex 0 0 1
- vertex 1 1 1
- vertex 1 0 1
- endloop
- endfacet
- facet normal 0 0 1
- outer loop
- vertex 0 0 1
- vertex 0 1 1
- vertex 1 1 1
- endloop
- endfacet
- facet normal 0 -1 0
- outer loop
- vertex 0 0 0
- vertex 1 0 1
- vertex 1 0 0
- endloop
- endfacet
- facet normal 0 -1 0
- outer loop
- vertex 0 0 0
- vertex 0 0 1
- vertex 1 0 1
- endloop
- endfacet
- facet normal 1 0 0
- outer loop
- vertex 1 0 0
- vertex 1 1 1
- vertex 1 1 0
- endloop
- endfacet
- facet normal 1 0 0
- outer loop
- vertex 1 0 0
- vertex 1 0 1
- vertex 1 1 1
- endloop
- endfacet
- facet normal 0 1 0
- outer loop
- vertex 0 1 0
- vertex 1 1 0
- vertex 1 1 1
- endloop
- endfacet
- facet normal 0 1 0
- outer loop
- vertex 0 1 0
- vertex 1 1 1
- vertex 0 1 1
- endloop
- endfacet
- facet normal -1 0 0
- outer loop
- vertex 0 0 0
- vertex 0 1 0
- vertex 0 1 1
- endloop
- endfacet
- facet normal -1 0 0
- outer loop
- vertex 0 0 0
- vertex 0 1 1
- vertex 0 0 1
- endloop
- endfacet
- endsolid cube
- """
- with tempfile.NamedTemporaryFile(suffix=".stl", delete=False, mode="w") as stl_file:
- stl_file.write(ascii_stl)
- stl_path = stl_file.name
- with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as png_file:
- png_path = png_file.name
- try:
- result = generate_stl_thumbnail(stl_path, png_path)
- assert result is True
- assert os.path.exists(png_path)
- # Check it's a valid PNG (starts with PNG magic bytes)
- with open(png_path, "rb") as f:
- header = f.read(8)
- assert header[:4] == b"\x89PNG"
- finally:
- os.unlink(stl_path)
- if os.path.exists(png_path):
- os.unlink(png_path)
- def test_generate_thumbnail_binary_stl(self):
- """Test generating thumbnail from binary STL file."""
- import struct
- from backend.app.services.stl_thumbnail import generate_stl_thumbnail
- # Create a simple binary STL cube (minimal version)
- # Binary STL format:
- # - 80 bytes header
- # - 4 bytes number of triangles (uint32)
- # - For each triangle:
- # - 12 bytes normal (3 floats)
- # - 36 bytes vertices (9 floats, 3 vertices x 3 coords)
- # - 2 bytes attribute byte count (usually 0)
- header = b"\x00" * 80 # Empty header
- num_triangles = 12 # A cube has 12 triangles (2 per face)
- # Define cube vertices
- vertices = [
- # Bottom face (z=0)
- ((0, 0, -1), [(0, 0, 0), (1, 0, 0), (1, 1, 0)]),
- ((0, 0, -1), [(0, 0, 0), (1, 1, 0), (0, 1, 0)]),
- # Top face (z=1)
- ((0, 0, 1), [(0, 0, 1), (1, 1, 1), (1, 0, 1)]),
- ((0, 0, 1), [(0, 0, 1), (0, 1, 1), (1, 1, 1)]),
- # Front face (y=0)
- ((0, -1, 0), [(0, 0, 0), (1, 0, 1), (1, 0, 0)]),
- ((0, -1, 0), [(0, 0, 0), (0, 0, 1), (1, 0, 1)]),
- # Back face (y=1)
- ((0, 1, 0), [(0, 1, 0), (1, 1, 0), (1, 1, 1)]),
- ((0, 1, 0), [(0, 1, 0), (1, 1, 1), (0, 1, 1)]),
- # Left face (x=0)
- ((-1, 0, 0), [(0, 0, 0), (0, 1, 0), (0, 1, 1)]),
- ((-1, 0, 0), [(0, 0, 0), (0, 1, 1), (0, 0, 1)]),
- # Right face (x=1)
- ((1, 0, 0), [(1, 0, 0), (1, 1, 1), (1, 1, 0)]),
- ((1, 0, 0), [(1, 0, 0), (1, 0, 1), (1, 1, 1)]),
- ]
- binary_data = header + struct.pack("<I", num_triangles)
- for normal, verts in vertices:
- binary_data += struct.pack("<fff", *normal) # Normal
- for v in verts:
- binary_data += struct.pack("<fff", *v) # Vertex
- binary_data += struct.pack("<H", 0) # Attribute byte count
- with tempfile.NamedTemporaryFile(suffix=".stl", delete=False, mode="wb") as stl_file:
- stl_file.write(binary_data)
- stl_path = stl_file.name
- with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as png_file:
- png_path = png_file.name
- try:
- result = generate_stl_thumbnail(stl_path, png_path)
- assert result is True
- assert os.path.exists(png_path)
- finally:
- os.unlink(stl_path)
- if os.path.exists(png_path):
- os.unlink(png_path)
- def test_generate_thumbnail_invalid_file(self):
- """Test handling of invalid STL file."""
- from backend.app.services.stl_thumbnail import generate_stl_thumbnail
- # Create invalid STL content
- with tempfile.NamedTemporaryFile(suffix=".stl", delete=False, mode="w") as stl_file:
- stl_file.write("This is not valid STL content")
- stl_path = stl_file.name
- with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as png_file:
- png_path = png_file.name
- try:
- result = generate_stl_thumbnail(stl_path, png_path)
- # Should return False for invalid file
- assert result is False
- finally:
- os.unlink(stl_path)
- if os.path.exists(png_path):
- os.unlink(png_path)
- def test_generate_thumbnail_nonexistent_file(self):
- """Test handling of nonexistent file."""
- from backend.app.services.stl_thumbnail import generate_stl_thumbnail
- result = generate_stl_thumbnail("/nonexistent/path/file.stl", "/tmp/output.png")
- assert result is False
- def test_generate_thumbnail_bytes_ascii(self):
- """Test generating thumbnail from ASCII STL bytes."""
- from backend.app.services.stl_thumbnail import generate_stl_thumbnail_bytes
- # Simple ASCII STL cube (same as above)
- ascii_stl = b"""solid cube
- facet normal 0 0 -1
- outer loop
- vertex 0 0 0
- vertex 1 0 0
- vertex 1 1 0
- endloop
- endfacet
- facet normal 0 0 1
- outer loop
- vertex 0 0 1
- vertex 1 0 1
- vertex 1 1 1
- endloop
- endfacet
- endsolid cube
- """
- result = generate_stl_thumbnail_bytes(ascii_stl)
- assert result is not None
- # Check it's a valid PNG
- assert result[:4] == b"\x89PNG"
- def test_generate_thumbnail_bytes_invalid(self):
- """Test handling of invalid STL bytes."""
- from backend.app.services.stl_thumbnail import generate_stl_thumbnail_bytes
- result = generate_stl_thumbnail_bytes(b"not valid stl data")
- assert result is None
- def test_generate_thumbnail_custom_size(self):
- """Test generating thumbnail with custom size."""
- from backend.app.services.stl_thumbnail import generate_stl_thumbnail
- ascii_stl = """solid cube
- facet normal 0 0 -1
- outer loop
- vertex 0 0 0
- vertex 1 0 0
- vertex 1 1 0
- endloop
- endfacet
- endsolid cube
- """
- with tempfile.NamedTemporaryFile(suffix=".stl", delete=False, mode="w") as stl_file:
- stl_file.write(ascii_stl)
- stl_path = stl_file.name
- with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as png_file:
- png_path = png_file.name
- try:
- result = generate_stl_thumbnail(stl_path, png_path, size=(128, 128))
- assert result is True
- assert os.path.exists(png_path)
- finally:
- os.unlink(stl_path)
- if os.path.exists(png_path):
- os.unlink(png_path)
- class TestMeshSimplification:
- """Tests for mesh simplification with large files."""
- def test_simplification_threshold(self):
- """Test that MAX_VERTICES constant is defined."""
- from backend.app.services.stl_thumbnail import MAX_VERTICES
- assert MAX_VERTICES == 100000
- def test_large_mesh_handling(self):
- """Test that large meshes are simplified (mocked)."""
- from backend.app.services.stl_thumbnail import generate_stl_thumbnail
- # Create a mock mesh with many vertices
- with patch("trimesh.load") as mock_load:
- mock_mesh = MagicMock()
- mock_mesh.vertices = MagicMock()
- mock_mesh.vertices.__len__ = MagicMock(return_value=200000) # Over threshold
- mock_mesh.faces = MagicMock()
- mock_mesh.faces.__len__ = MagicMock(return_value=400000)
- mock_simplified = MagicMock()
- mock_simplified.vertices = [[0, 0, 0], [1, 0, 0], [0, 1, 0]]
- mock_simplified.faces = [[0, 1, 2]]
- mock_mesh.simplify_quadric_decimation.return_value = mock_simplified
- mock_load.return_value = mock_mesh
- with tempfile.NamedTemporaryFile(suffix=".stl", delete=False) as stl_file:
- stl_file.write(b"dummy")
- stl_path = stl_file.name
- with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as png_file:
- png_path = png_file.name
- try:
- # This will fail but we can verify simplification was called
- generate_stl_thumbnail(stl_path, png_path)
- # The mock should have been called for simplification
- mock_mesh.simplify_quadric_decimation.assert_called()
- finally:
- os.unlink(stl_path)
- if os.path.exists(png_path):
- os.unlink(png_path)
|