"""
Integration tests for Retopology Evaluation (Feature 29).
These tests verify the actual implementation of evaluate_retopology
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 TestEvaluateRetopology:
"""Tests for evaluate_retopology tool."""
def _create_high_low_poly_setup(self, real_blender_connection):
"""Helper to create a high-poly and low-poly mesh pair."""
send_command(real_blender_connection, "execute_code", {
"code": """
import bpy
# Clear scene
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete()
# Create high-poly mesh (subdivided sphere)
bpy.ops.mesh.primitive_uv_sphere_add(radius=1, location=(0, 0, 0))
hp = bpy.context.active_object
hp.name = "HighPoly"
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.subdivide(number_cuts=2)
bpy.ops.object.mode_set(mode='OBJECT')
# Create low-poly mesh (simple sphere near high-poly)
bpy.ops.mesh.primitive_uv_sphere_add(radius=1.005, segments=16, ring_count=8, location=(0, 0, 0))
lp = bpy.context.active_object
lp.name = "LowPoly"
"""
})
def test_evaluate_retopology_basic(self, real_blender_connection):
"""
Feature: Evaluate retopology with default thresholds.
"""
self._create_high_low_poly_setup(real_blender_connection)
response = send_command(real_blender_connection, "evaluate_retopology", {
"high_poly": "HighPoly",
"low_poly": "LowPoly"
})
assert response["status"] == "success", f"Command failed: {response}"
result = response["result"]
# Verify required fields
required_fields = [
"high_poly", "low_poly",
"high_face_count", "low_face_count",
"quad_ratio", "valence_ratio",
"distance_max", "distance_avg", "distance_coverage",
"passed", "passed_criteria", "issues"
]
for field in required_fields:
assert field in result, f"Missing field: {field}"
print(f"Evaluation Result: passed={result['passed']}, "
f"quad_ratio={result['quad_ratio']}, "
f"distance_max={result['distance_max']}")
def test_evaluate_good_retopology_passes(self, real_blender_connection):
"""
Feature: Passing evaluation with good retopology.
"""
# Create ideal scenario: low-poly wraps high-poly perfectly
send_command(real_blender_connection, "execute_code", {
"code": """
import bpy
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete()
# High-poly cube
bpy.ops.mesh.primitive_cube_add(size=2, location=(0, 0, 0))
hp = bpy.context.active_object
hp.name = "HighPoly"
# Low-poly: same cube (perfect match)
bpy.ops.mesh.primitive_cube_add(size=2, location=(0, 0, 0))
lp = bpy.context.active_object
lp.name = "LowPoly"
"""
})
# Use very relaxed thresholds for identical geometry
response = send_command(real_blender_connection, "evaluate_retopology", {
"high_poly": "HighPoly",
"low_poly": "LowPoly",
"quad_threshold": 0.9,
"distance_threshold": 0.1,
"coverage_threshold": 0.9,
"valence_threshold": 0.0 # Cube has valence-3 corners
})
assert response["status"] == "success"
result = response["result"]
# Identical geometry should have very small distance
assert result["distance_max"] < 0.01, \
f"Identical cubes should have near-zero distance, got {result['distance_max']}"
# Should have 100% quads
assert result["quad_ratio"] >= 0.99, \
f"Cubes should have 100% quads, got {result['quad_ratio']}"
def test_evaluate_low_quad_ratio_fails(self, real_blender_connection):
"""
Feature: Failing evaluation due to low quad ratio.
"""
send_command(real_blender_connection, "execute_code", {
"code": """
import bpy
bpy.ops.object.select_all(action='SELECT')
bpy.ops.object.delete()
# High-poly quad mesh
bpy.ops.mesh.primitive_cube_add(size=2)
hp = bpy.context.active_object
hp.name = "HighPoly"
# Low-poly: triangulated mesh
bpy.ops.mesh.primitive_cube_add(size=2)
lp = bpy.context.active_object
lp.name = "LowPoly"
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, "evaluate_retopology", {
"high_poly": "HighPoly",
"low_poly": "LowPoly",
"quad_threshold": 0.85,
"valence_threshold": 0.0 # Don't fail on valence
})
assert response["status"] == "success"
result = response["result"]
# Triangulated mesh has 0% quads
assert result["quad_ratio"] == 0.0, \
f"Triangulated mesh should have 0% quads, got {result['quad_ratio']}"
# Should fail quad_ratio criterion
assert not result["passed_criteria"]["quad_ratio"], \
"Quad ratio criterion should fail for triangulated mesh"
def test_evaluate_with_custom_thresholds(self, real_blender_connection):
"""
Feature: Custom threshold configuration.
"""
self._create_high_low_poly_setup(real_blender_connection)
# Test with very relaxed thresholds
response = send_command(real_blender_connection, "evaluate_retopology", {
"high_poly": "HighPoly",
"low_poly": "LowPoly",
"quad_threshold": 0.1,
"distance_threshold": 1.0,
"coverage_threshold": 0.1,
"valence_threshold": 0.1
})
assert response["status"] == "success"
result = response["result"]
# Verify thresholds were used
thresholds = result.get("thresholds_used", {})
assert thresholds.get("quad_threshold") == 0.1, "Custom quad_threshold not used"
assert thresholds.get("distance_threshold") == 1.0, "Custom distance_threshold not used"
def test_evaluate_nonexistent_high_poly_fails(self, real_blender_connection):
"""
Feature: Evaluate with non-existent high-poly fails.
"""
# Create only low-poly
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.context.active_object.name = "LowPoly"
"""
})
response = send_command(real_blender_connection, "evaluate_retopology", {
"high_poly": "NonExistent",
"low_poly": "LowPoly"
})
assert response["status"] == "success"
result = response["result"]
assert "error" in result, "Expected error for non-existent object"
assert "not found" in result["error"].lower() or "NonExistent" in result["error"], \
f"Error should mention object not found: {result['error']}"
def test_evaluate_nonexistent_low_poly_fails(self, real_blender_connection):
"""
Feature: Evaluate with non-existent low-poly fails.
"""
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.context.active_object.name = "HighPoly"
"""
})
response = send_command(real_blender_connection, "evaluate_retopology", {
"high_poly": "HighPoly",
"low_poly": "NonExistent"
})
assert response["status"] == "success"
result = response["result"]
assert "error" in result, "Expected error for non-existent object"
def test_evaluate_non_mesh_objects_fails(self, real_blender_connection):
"""
Feature: Evaluate with non-mesh objects fails.
"""
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)
bpy.context.active_object.name = "CurveObj"
bpy.ops.mesh.primitive_cube_add(size=2)
bpy.context.active_object.name = "LowPoly"
"""
})
response = send_command(real_blender_connection, "evaluate_retopology", {
"high_poly": "CurveObj",
"low_poly": "LowPoly"
})
assert response["status"] == "success"
result = response["result"]
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_evaluate_identical_meshes(self, real_blender_connection):
"""
Feature: Evaluate identical meshes.
When evaluating same mesh against itself, should have perfect metrics.
"""
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.context.active_object.name = "TestMesh"
"""
})
response = send_command(real_blender_connection, "evaluate_retopology", {
"high_poly": "TestMesh",
"low_poly": "TestMesh",
"valence_threshold": 0.0 # Cube vertices have valence 3
})
assert response["status"] == "success"
result = response["result"]
# Same mesh should have 0 distance
assert result["distance_max"] == 0 or result["distance_max"] < 0.0001, \
f"Same mesh should have 0 distance, got {result['distance_max']}"
# Same mesh should have 100% coverage
assert result["distance_coverage"] >= 0.99, \
f"Same mesh should have ~100% coverage, got {result['distance_coverage']}"
def test_evaluate_provides_issues_list(self, real_blender_connection):
"""
Feature: Evaluation provides actionable feedback.
"""
self._create_high_low_poly_setup(real_blender_connection)
response = send_command(real_blender_connection, "evaluate_retopology", {
"high_poly": "HighPoly",
"low_poly": "LowPoly",
"quad_threshold": 0.99, # Very strict - likely to fail
"distance_threshold": 0.0001, # Very strict
"coverage_threshold": 0.99,
"valence_threshold": 0.99
})
assert response["status"] == "success"
result = response["result"]
# With strict thresholds, should have issues
assert "issues" in result, "Result should have issues list"
assert isinstance(result["issues"], list), "Issues should be a list"
# If evaluation failed, issues should explain why
if not result["passed"]:
assert len(result["issues"]) > 0, \
"Failed evaluation should have at least one issue"
print(f"Issues found: {result['issues']}")
def test_evaluate_returns_quality_grade(self, real_blender_connection):
"""
Feature: Detailed metrics breakdown includes quality grade.
"""
self._create_high_low_poly_setup(real_blender_connection)
response = send_command(real_blender_connection, "evaluate_retopology", {
"high_poly": "HighPoly",
"low_poly": "LowPoly"
})
assert response["status"] == "success"
result = response["result"]
assert "quality_grade" in result, "Missing quality_grade"
assert result["quality_grade"] in ["A", "B", "C", "D", "F"], \
f"Invalid quality grade: {result['quality_grade']}"
def test_evaluate_returns_polycount_ratio(self, real_blender_connection):
"""
Feature: Returns polycount reduction ratio.
"""
self._create_high_low_poly_setup(real_blender_connection)
response = send_command(real_blender_connection, "evaluate_retopology", {
"high_poly": "HighPoly",
"low_poly": "LowPoly"
})
assert response["status"] == "success"
result = response["result"]
assert "polycount_ratio" in result, "Missing polycount_ratio"
assert "high_face_count" in result, "Missing high_face_count"
assert "low_face_count" in result, "Missing low_face_count"
# Low poly should have fewer faces than high poly
assert result["low_face_count"] <= result["high_face_count"], \
f"Low poly ({result['low_face_count']}) should have <= faces than high poly ({result['high_face_count']})"
print(f"Polycount: HP={result['high_face_count']}, LP={result['low_face_count']}, "
f"ratio={result['polycount_ratio']}")