"""
Integration tests for Topology Quality Analysis (Feature 28).
These tests verify the actual implementation of get_topology_quality
and analyze_mesh_regions against real Blender meshes.
Testing criteria:
1. Method implementation works
2. Business logic matches BDD scenarios
3. Output is correct
"""
import pytest
from tests.integration.conftest import send_command
class TestGetTopologyQuality:
"""Tests for get_topology_quality tool."""
def test_topology_quality_on_cube(self, real_blender_connection):
"""
Feature: Get topology quality for active mesh
A cube should have 100% quads, all valence-4 vertices, and high quality score.
"""
# Create a cube (6 quad faces, 8 vertices with valence 3)
send_command(real_blender_connection, "execute_code", {
"code": "import bpy; bpy.ops.object.select_all(action='SELECT'); bpy.ops.object.delete()"
})
send_command(real_blender_connection, "execute_code", {
"code": "import bpy; bpy.ops.mesh.primitive_cube_add(size=2)"
})
# Get topology quality
response = send_command(real_blender_connection, "get_topology_quality", {})
assert response["status"] == "success", f"Command failed: {response}"
result = response["result"]
# Verify cube metrics
assert result["total_faces"] == 6, f"Expected 6 faces, got {result['total_faces']}"
assert result["quad_count"] == 6, f"Expected 6 quads, got {result['quad_count']}"
assert result["quad_percentage"] == 100.0, f"Expected 100% quads, got {result['quad_percentage']}"
assert result["tri_count"] == 0, "Cube should have no triangles"
assert result["ngon_count"] == 0, "Cube should have no ngons"
assert result["total_vertices"] == 8, f"Expected 8 vertices, got {result['total_vertices']}"
# Cube vertices have valence 3 (corners), so pole_count should be 8
assert result["pole_count"] == 8, f"Expected 8 poles (valence-3), got {result['pole_count']}"
print(f"Topology Quality Result: {result}")
def test_topology_quality_returns_all_metrics(self, real_blender_connection):
"""
Feature: Verify all required metrics are returned.
"""
# Create a mesh
send_command(real_blender_connection, "execute_code", {
"code": "import bpy; bpy.ops.object.select_all(action='SELECT'); bpy.ops.object.delete()"
})
send_command(real_blender_connection, "execute_code", {
"code": "import bpy; bpy.ops.mesh.primitive_plane_add(size=2)"
})
response = send_command(real_blender_connection, "get_topology_quality", {})
assert response["status"] == "success"
result = response["result"]
# Verify all required fields are present
required_fields = [
"object_name",
"total_faces",
"total_vertices",
"quad_count",
"tri_count",
"ngon_count",
"quad_percentage",
"tri_percentage",
"ngon_percentage",
"valence_distribution",
"valence_4_ratio",
"pole_count",
"pole_locations",
"avg_aspect_ratio",
"bad_aspect_count",
"quality_score"
]
for field in required_fields:
assert field in result, f"Missing field: {field}"
# Verify quality_score is in valid range
assert 0 <= result["quality_score"] <= 100, \
f"Quality score {result['quality_score']} out of range 0-100"
def test_topology_quality_on_subdivided_mesh(self, real_blender_connection):
"""
Feature: Quality score reflects high quad percentage.
A subdivided mesh should have high quality metrics.
"""
# Create and subdivide a cube
send_command(real_blender_connection, "execute_code", {
"code": """
import bpy
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete()
bpy.ops.mesh.primitive_cube_add(size=2)
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.subdivide(number_cuts=2)
bpy.ops.object.mode_set(mode='OBJECT')
"""
})
response = send_command(real_blender_connection, "get_topology_quality", {})
assert response["status"] == "success"
result = response["result"]
# Subdivided cube should have 100% quads
assert result["quad_percentage"] == 100.0, \
f"Subdivided cube should have 100% quads, got {result['quad_percentage']}"
# Quality score should be high
assert result["quality_score"] >= 70, \
f"Expected high quality score, got {result['quality_score']}"
def test_topology_quality_with_triangles(self, real_blender_connection):
"""
Feature: Quality analysis on mesh with mixed face types.
"""
# Create a mesh and triangulate it
send_command(real_blender_connection, "execute_code", {
"code": """
import bpy
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete()
bpy.ops.mesh.primitive_cube_add(size=2)
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.quads_convert_to_tris(quad_method='BEAUTY')
bpy.ops.object.mode_set(mode='OBJECT')
"""
})
response = send_command(real_blender_connection, "get_topology_quality", {})
assert response["status"] == "success"
result = response["result"]
# Should have triangles, no quads
assert result["tri_count"] > 0, "Expected triangles after triangulation"
assert result["quad_count"] == 0, "Expected no quads after triangulation"
assert result["tri_percentage"] == 100.0, "Expected 100% triangles"
assert result["quad_percentage"] == 0.0, "Expected 0% quads"
# Quality score should be lower for all-tri mesh
assert result["quality_score"] < 50, \
f"All-tri mesh should have lower quality score, got {result['quality_score']}"
def test_topology_quality_on_non_mesh_fails(self, real_blender_connection):
"""
Feature: Quality analysis on non-mesh object fails gracefully.
"""
# Create a curve
send_command(real_blender_connection, "execute_code", {
"code": """
import bpy
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete()
bpy.ops.curve.primitive_bezier_circle_add(radius=1)
"""
})
response = send_command(real_blender_connection, "get_topology_quality", {})
assert response["status"] == "success"
result = response["result"]
# Should return error for non-mesh
assert "error" in result, "Expected error for non-mesh object"
assert "mesh" in result["error"].lower(), \
f"Error should mention mesh requirement: {result['error']}"
def test_topology_quality_valence_distribution(self, real_blender_connection):
"""
Feature: Valence distribution analysis.
"""
# Create a grid (has valence-4 vertices except edges/corners)
send_command(real_blender_connection, "execute_code", {
"code": """
import bpy
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete()
bpy.ops.mesh.primitive_grid_add(x_subdivisions=4, y_subdivisions=4, size=2)
"""
})
response = send_command(real_blender_connection, "get_topology_quality", {})
assert response["status"] == "success"
result = response["result"]
# Verify valence distribution exists
assert "valence_distribution" in result
valence_dist = result["valence_distribution"]
# Grid should have regular internal vertices (valence 4)
# and irregular edge/corner vertices (valence 2-3)
assert "4" in str(valence_dist) or 4 in valence_dist, \
f"Expected valence 4 in distribution: {valence_dist}"
# valence_4_ratio should be a valid percentage
assert 0 <= result["valence_4_ratio"] <= 100, \
f"valence_4_ratio should be 0-100, got {result['valence_4_ratio']}"
class TestAnalyzeMeshRegions:
"""Tests for analyze_mesh_regions tool."""
def test_analyze_regions_on_cube(self, real_blender_connection):
"""
Feature: Analyze mesh by spatial regions.
"""
# Create a cube
send_command(real_blender_connection, "execute_code", {
"code": """
import bpy
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete()
bpy.ops.mesh.primitive_cube_add(size=2)
"""
})
response = send_command(real_blender_connection, "analyze_mesh_regions", {})
assert response["status"] == "success", f"Command failed: {response}"
result = response["result"]
# Verify required fields
assert "regions" in result, "Missing regions in result"
assert "problem_zones" in result, "Missing problem_zones in result"
assert "total_regions" in result, "Missing total_regions in result"
assert "object_name" in result, "Missing object_name in result"
# Cube divided into 8 regions (2x2x2 grid)
assert result["total_regions"] <= 8, \
f"Expected up to 8 regions, got {result['total_regions']}"
def test_analyze_regions_returns_quality_metrics(self, real_blender_connection):
"""
Feature: Each region has quality metrics.
"""
# Create and subdivide a mesh for more faces
send_command(real_blender_connection, "execute_code", {
"code": """
import bpy
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete()
bpy.ops.mesh.primitive_cube_add(size=2)
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.subdivide(number_cuts=3)
bpy.ops.object.mode_set(mode='OBJECT')
"""
})
response = send_command(real_blender_connection, "analyze_mesh_regions", {})
assert response["status"] == "success"
result = response["result"]
regions = result["regions"]
assert len(regions) > 0, "Expected at least one region"
# Check first region has required fields
for region in regions:
assert "id" in region, "Region missing id"
assert "center" in region, "Region missing center"
assert "face_count" in region, "Region missing face_count"
assert "quality_score" in region, "Region missing quality_score"
assert "quad_percentage" in region, "Region missing quad_percentage"
# Center should be 3D coordinates
center = region["center"]
assert len(center) == 3, f"Center should have 3 coordinates: {center}"
# Quality score in valid range
assert 0 <= region["quality_score"] <= 100, \
f"Quality score {region['quality_score']} out of range"
def test_analyze_regions_with_mixed_topology(self, real_blender_connection):
"""
Feature: Identify problem areas via regional analysis.
"""
# Create mesh with mixed topology (some tris, some quads)
send_command(real_blender_connection, "execute_code", {
"code": """
import bpy
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete()
bpy.ops.mesh.primitive_cube_add(size=2)
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.subdivide(number_cuts=2)
# Select some faces and triangulate
bpy.ops.mesh.select_all(action='DESELECT')
bpy.ops.object.mode_set(mode='OBJECT')
obj = bpy.context.active_object
# Select top faces only
for i, face in enumerate(obj.data.polygons):
if face.center[2] > 0.5:
face.select = True
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.quads_convert_to_tris()
bpy.ops.object.mode_set(mode='OBJECT')
"""
})
response = send_command(real_blender_connection, "analyze_mesh_regions", {})
assert response["status"] == "success"
result = response["result"]
# Should have some regions with issues
regions = result["regions"]
has_problem_region = any(r.get("issues", []) for r in regions)
# Either problem_zones or regions with issues should exist
# (depends on how quads distribute across regions)
print(f"Regional Analysis: {len(regions)} regions, "
f"{len(result['problem_zones'])} problem zones")
def test_analyze_regions_on_non_mesh_fails(self, real_blender_connection):
"""
Feature: Regional analysis on non-mesh fails gracefully.
"""
send_command(real_blender_connection, "execute_code", {
"code": """
import bpy
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete()
bpy.ops.curve.primitive_bezier_curve_add(radius=1)
"""
})
response = send_command(real_blender_connection, "analyze_mesh_regions", {})
assert response["status"] == "success"
result = response["result"]
assert "error" in result, "Expected error for non-mesh"
assert "mesh" in result["error"].lower(), \
f"Error should mention mesh: {result['error']}"
def test_analyze_regions_coordinates_for_navigation(self, real_blender_connection):
"""
Feature: Region coordinates allow navigation via focus_view_on_point.
"""
# Create a mesh with known position
send_command(real_blender_connection, "execute_code", {
"code": """
import bpy
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete()
bpy.ops.mesh.primitive_cube_add(size=2, location=(0, 0, 0))
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.subdivide(number_cuts=2)
bpy.ops.object.mode_set(mode='OBJECT')
"""
})
response = send_command(real_blender_connection, "analyze_mesh_regions", {})
assert response["status"] == "success"
result = response["result"]
regions = result["regions"]
if len(regions) > 0:
# Use first region center for focus_view_on_point
center = regions[0]["center"]
focus_response = send_command(real_blender_connection, "focus_view_on_point", {
"x": center[0],
"y": center[1],
"z": center[2],
"distance": 3.0
})
assert focus_response["status"] == "success", \
f"Failed to focus on region center: {focus_response}"
print(f"Successfully focused on region center: {center}")