feat(body_mesh): Add edge UV data for facial and hair mesh components
- Add edge UV coordinate data for N00_000_00_FaceEyelash_00_FACE mesh instance with 112 edges - Add edge UV coordinate data for N00_000_00_HairBack_00_HAIR mesh instance with 800 edges - Update body_mesh module initialization and operations to support new mesh data - Enhance panel UI and utility functions for mesh edge UV handling - Add comprehensive UV matching test suite for validation - Update registration module to include new mesh components - Improve test utilities for edge UV data verification
This commit is contained in:
728
tests/test_uv_matching.py
Normal file
728
tests/test_uv_matching.py
Normal file
@@ -0,0 +1,728 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
"""
|
||||
Standalone property-based tests for UV edge matching functionality.
|
||||
|
||||
Feature: vrm-uv-edge-merger
|
||||
Property 5: UV Edge Matching Precision
|
||||
|
||||
These tests can run without Blender dependencies.
|
||||
"""
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
from hypothesis import given, settings
|
||||
from hypothesis import strategies as st
|
||||
|
||||
|
||||
# UV坐标精度(小数位数)- copied from utils.py
|
||||
UV_PRECISION = 6
|
||||
|
||||
|
||||
def normalize_uv_pair(
|
||||
uv1: tuple[float, float], uv2: tuple[float, float]
|
||||
) -> tuple[tuple[float, float], tuple[float, float]]:
|
||||
"""
|
||||
标准化UV坐标对(较小的在前)
|
||||
|
||||
通过将较小的UV坐标放在前面,确保相同的边无论顶点顺序如何都能匹配。
|
||||
使用6位小数精度进行比较和存储。
|
||||
|
||||
Args:
|
||||
uv1: 第一个UV坐标 (u, v)
|
||||
uv2: 第二个UV坐标 (u, v)
|
||||
|
||||
Returns:
|
||||
标准化后的UV对 ((u1,v1), (u2,v2)),较小的坐标在前
|
||||
"""
|
||||
# 四舍五入到指定精度
|
||||
uv1_rounded = (round(uv1[0], UV_PRECISION), round(uv1[1], UV_PRECISION))
|
||||
uv2_rounded = (round(uv2[0], UV_PRECISION), round(uv2[1], UV_PRECISION))
|
||||
|
||||
# 比较并排序,较小的在前
|
||||
if uv1_rounded <= uv2_rounded:
|
||||
return (uv1_rounded, uv2_rounded)
|
||||
return (uv2_rounded, uv1_rounded)
|
||||
|
||||
|
||||
class TestUvEdgeMatchingPrecision:
|
||||
"""
|
||||
Property 5: UV边匹配精度
|
||||
*For any* edge with UV coordinates, the matching function SHALL correctly identify
|
||||
the edge if its UV coordinates (rounded to 6 decimal places) exist in the target UV set.
|
||||
**Validates: Requirements 4.2, 4.3, 4.4**
|
||||
"""
|
||||
|
||||
@given(
|
||||
st.tuples(
|
||||
st.floats(min_value=0.0, max_value=1.0, allow_nan=False, allow_infinity=False),
|
||||
st.floats(min_value=0.0, max_value=1.0, allow_nan=False, allow_infinity=False),
|
||||
),
|
||||
st.tuples(
|
||||
st.floats(min_value=0.0, max_value=1.0, allow_nan=False, allow_infinity=False),
|
||||
st.floats(min_value=0.0, max_value=1.0, allow_nan=False, allow_infinity=False),
|
||||
),
|
||||
st.floats(min_value=-1e-7, max_value=1e-7, allow_nan=False, allow_infinity=False),
|
||||
)
|
||||
@settings(max_examples=100)
|
||||
def test_uv_matching_precision_property(
|
||||
self,
|
||||
uv1: tuple[float, float],
|
||||
uv2: tuple[float, float],
|
||||
noise: float,
|
||||
) -> None:
|
||||
"""
|
||||
Feature: vrm-uv-edge-merger, Property 5: UV Edge Matching Precision
|
||||
For any UV coordinate pair, adding noise smaller than 6 decimal precision
|
||||
should still result in a match after normalization.
|
||||
"""
|
||||
# Create the normalized reference UV pair
|
||||
normalized_ref = normalize_uv_pair(uv1, uv2)
|
||||
|
||||
# Add small noise (within 6 decimal precision tolerance)
|
||||
uv1_noisy = (uv1[0] + noise, uv1[1] + noise)
|
||||
uv2_noisy = (uv2[0] + noise, uv2[1] + noise)
|
||||
|
||||
# Normalize the noisy pair
|
||||
normalized_noisy = normalize_uv_pair(uv1_noisy, uv2_noisy)
|
||||
|
||||
# Create target UV set with the reference
|
||||
target_uvs = {normalized_ref}
|
||||
|
||||
# The noisy normalized pair should match the reference
|
||||
# because the noise is within 6 decimal precision
|
||||
matches = normalized_noisy in target_uvs
|
||||
|
||||
# Calculate expected match based on rounding behavior
|
||||
# If noise is small enough, rounding should produce same result
|
||||
expected_match = normalized_ref == normalized_noisy
|
||||
|
||||
assert matches == expected_match, (
|
||||
f"UV matching inconsistency: "
|
||||
f"normalized_ref={normalized_ref}, normalized_noisy={normalized_noisy}, "
|
||||
f"noise={noise}, matches={matches}, expected={expected_match}"
|
||||
)
|
||||
|
||||
@given(
|
||||
st.tuples(
|
||||
st.floats(min_value=0.0, max_value=1.0, allow_nan=False, allow_infinity=False),
|
||||
st.floats(min_value=0.0, max_value=1.0, allow_nan=False, allow_infinity=False),
|
||||
),
|
||||
st.tuples(
|
||||
st.floats(min_value=0.0, max_value=1.0, allow_nan=False, allow_infinity=False),
|
||||
st.floats(min_value=0.0, max_value=1.0, allow_nan=False, allow_infinity=False),
|
||||
),
|
||||
)
|
||||
@settings(max_examples=100)
|
||||
def test_uv_matching_set_membership_property(
|
||||
self,
|
||||
uv1: tuple[float, float],
|
||||
uv2: tuple[float, float],
|
||||
) -> None:
|
||||
"""
|
||||
Feature: vrm-uv-edge-merger, Property 5: UV Edge Matching Precision
|
||||
For any UV coordinate pair, the normalized pair should be found in a target
|
||||
set containing that same normalized pair, regardless of original order.
|
||||
"""
|
||||
# Normalize in both orders
|
||||
normalized_forward = normalize_uv_pair(uv1, uv2)
|
||||
normalized_reverse = normalize_uv_pair(uv2, uv1)
|
||||
|
||||
# Create target set with forward normalization
|
||||
target_uvs = {normalized_forward}
|
||||
|
||||
# Both should match (symmetry property ensures this)
|
||||
assert normalized_forward in target_uvs, (
|
||||
f"Forward normalized pair {normalized_forward} not found in target set"
|
||||
)
|
||||
assert normalized_reverse in target_uvs, (
|
||||
f"Reverse normalized pair {normalized_reverse} not found in target set "
|
||||
f"(forward was {normalized_forward})"
|
||||
)
|
||||
|
||||
@given(
|
||||
st.tuples(
|
||||
st.floats(min_value=0.0, max_value=1.0, allow_nan=False, allow_infinity=False),
|
||||
st.floats(min_value=0.0, max_value=1.0, allow_nan=False, allow_infinity=False),
|
||||
),
|
||||
st.tuples(
|
||||
st.floats(min_value=0.0, max_value=1.0, allow_nan=False, allow_infinity=False),
|
||||
st.floats(min_value=0.0, max_value=1.0, allow_nan=False, allow_infinity=False),
|
||||
),
|
||||
)
|
||||
@settings(max_examples=100)
|
||||
def test_uv_precision_rounding_property(
|
||||
self,
|
||||
uv1: tuple[float, float],
|
||||
uv2: tuple[float, float],
|
||||
) -> None:
|
||||
"""
|
||||
Feature: vrm-uv-edge-merger, Property 5: UV Edge Matching Precision
|
||||
For any UV coordinate pair, the normalized result should have coordinates
|
||||
rounded to exactly 6 decimal places.
|
||||
"""
|
||||
normalized = normalize_uv_pair(uv1, uv2)
|
||||
|
||||
# Check that all coordinates are rounded to 6 decimal places
|
||||
for uv in normalized:
|
||||
for coord in uv:
|
||||
# Verify rounding by checking round(coord, 6) == coord
|
||||
assert round(coord, UV_PRECISION) == coord, (
|
||||
f"Coordinate {coord} is not rounded to {UV_PRECISION} decimal places"
|
||||
)
|
||||
|
||||
|
||||
def select_material_faces_impl(
|
||||
polygons_material_indices: list[int],
|
||||
material_index: int,
|
||||
) -> set[int]:
|
||||
"""
|
||||
获取指定材质索引的所有面的索引集合
|
||||
|
||||
这是一个纯函数实现,用于测试材质面选择的完整性。
|
||||
|
||||
Args:
|
||||
polygons_material_indices: 每个面的材质索引列表
|
||||
material_index: 目标材质索引
|
||||
|
||||
Returns:
|
||||
使用该材质的面索引集合
|
||||
"""
|
||||
result: set[int] = set()
|
||||
for i, mat_idx in enumerate(polygons_material_indices):
|
||||
if mat_idx == material_index:
|
||||
result.add(i)
|
||||
return result
|
||||
|
||||
|
||||
class TestMaterialFaceSelectionCompleteness:
|
||||
"""
|
||||
Property 6: Material Face Selection Completeness
|
||||
*For any* mesh and material index, the face selection function SHALL select
|
||||
exactly all faces that have the specified material index, and no others.
|
||||
**Validates: Requirements 3.2**
|
||||
"""
|
||||
|
||||
@given(
|
||||
st.lists(
|
||||
st.integers(min_value=0, max_value=5),
|
||||
min_size=1,
|
||||
max_size=50,
|
||||
),
|
||||
st.integers(min_value=0, max_value=5),
|
||||
)
|
||||
@settings(max_examples=100)
|
||||
def test_material_face_selection_completeness_property(
|
||||
self,
|
||||
material_indices: list[int],
|
||||
target_material_index: int,
|
||||
) -> None:
|
||||
"""
|
||||
Feature: vrm-uv-edge-merger, Property 6: Material Face Selection Completeness
|
||||
For any mesh with faces having various material indices, select_material_faces
|
||||
returns exactly the set of face indices that have the specified material index.
|
||||
"""
|
||||
# Call the function under test
|
||||
result = select_material_faces_impl(material_indices, target_material_index)
|
||||
|
||||
# Calculate expected result: indices of faces with the target material
|
||||
expected = {
|
||||
i for i, mat_idx in enumerate(material_indices)
|
||||
if mat_idx == target_material_index
|
||||
}
|
||||
|
||||
# Verify completeness: result should contain exactly the expected faces
|
||||
assert result == expected, (
|
||||
f"select_material_faces returned {result}, expected {expected}. "
|
||||
f"Material indices: {material_indices}, target: {target_material_index}"
|
||||
)
|
||||
|
||||
@given(
|
||||
st.lists(
|
||||
st.integers(min_value=0, max_value=5),
|
||||
min_size=0,
|
||||
max_size=50,
|
||||
),
|
||||
)
|
||||
@settings(max_examples=100)
|
||||
def test_material_face_selection_no_false_positives_property(
|
||||
self,
|
||||
material_indices: list[int],
|
||||
) -> None:
|
||||
"""
|
||||
Feature: vrm-uv-edge-merger, Property 6: Material Face Selection Completeness
|
||||
For any mesh, selecting faces for a material index that doesn't exist
|
||||
should return an empty set.
|
||||
"""
|
||||
# Use a material index that definitely doesn't exist
|
||||
non_existent_index = max(material_indices, default=-1) + 10
|
||||
|
||||
# Call the function under test
|
||||
result = select_material_faces_impl(material_indices, non_existent_index)
|
||||
|
||||
# Should return empty set for non-existent material
|
||||
assert result == set(), (
|
||||
f"select_material_faces returned {result} for non-existent material "
|
||||
f"index {non_existent_index}, expected empty set"
|
||||
)
|
||||
|
||||
@given(
|
||||
st.lists(
|
||||
st.integers(min_value=0, max_value=5),
|
||||
min_size=1,
|
||||
max_size=50,
|
||||
),
|
||||
st.integers(min_value=0, max_value=5),
|
||||
)
|
||||
@settings(max_examples=100)
|
||||
def test_material_face_selection_no_duplicates_property(
|
||||
self,
|
||||
material_indices: list[int],
|
||||
target_material_index: int,
|
||||
) -> None:
|
||||
"""
|
||||
Feature: vrm-uv-edge-merger, Property 6: Material Face Selection Completeness
|
||||
For any mesh, the selected face indices should be unique (no duplicates).
|
||||
"""
|
||||
result = select_material_faces_impl(material_indices, target_material_index)
|
||||
|
||||
# Since result is a set, it inherently has no duplicates
|
||||
# But we verify the count matches expected
|
||||
expected_count = sum(
|
||||
1 for mat_idx in material_indices if mat_idx == target_material_index
|
||||
)
|
||||
|
||||
assert len(result) == expected_count, (
|
||||
f"Expected {expected_count} faces, got {len(result)}. "
|
||||
f"Material indices: {material_indices}, target: {target_material_index}"
|
||||
)
|
||||
|
||||
@given(
|
||||
st.lists(
|
||||
st.integers(min_value=0, max_value=5),
|
||||
min_size=1,
|
||||
max_size=50,
|
||||
),
|
||||
st.integers(min_value=0, max_value=5),
|
||||
)
|
||||
@settings(max_examples=100)
|
||||
def test_material_face_selection_valid_indices_property(
|
||||
self,
|
||||
material_indices: list[int],
|
||||
target_material_index: int,
|
||||
) -> None:
|
||||
"""
|
||||
Feature: vrm-uv-edge-merger, Property 6: Material Face Selection Completeness
|
||||
For any mesh, all returned face indices should be valid (within bounds).
|
||||
"""
|
||||
result = select_material_faces_impl(material_indices, target_material_index)
|
||||
|
||||
# All indices should be valid
|
||||
for face_idx in result:
|
||||
assert 0 <= face_idx < len(material_indices), (
|
||||
f"Invalid face index {face_idx} for mesh with "
|
||||
f"{len(material_indices)} faces"
|
||||
)
|
||||
|
||||
|
||||
def find_body_mesh_impl(children: list[tuple[str, str]]) -> bool:
|
||||
"""
|
||||
检查子对象列表中是否存在名为"Body"的MESH对象
|
||||
|
||||
Args:
|
||||
children: 子对象列表,每个元素为 (name, type) 元组
|
||||
|
||||
Returns:
|
||||
如果存在名为"Body"的MESH对象返回True,否则返回False
|
||||
"""
|
||||
for name, obj_type in children:
|
||||
if name == "Body" and obj_type == "MESH":
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def find_face_mesh_impl(children: list[tuple[str, str]]) -> bool:
|
||||
"""
|
||||
检查子对象列表中是否存在名为"Face"的MESH对象
|
||||
|
||||
Args:
|
||||
children: 子对象列表,每个元素为 (name, type) 元组
|
||||
|
||||
Returns:
|
||||
如果存在名为"Face"的MESH对象返回True,否则返回False
|
||||
"""
|
||||
for name, obj_type in children:
|
||||
if name == "Face" and obj_type == "MESH":
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def is_valid_vrm_structure(children: list[tuple[str, str]]) -> bool:
|
||||
"""
|
||||
检查子对象列表是否构成有效的VRM结构
|
||||
|
||||
Args:
|
||||
children: 子对象列表,每个元素为 (name, type) 元组
|
||||
|
||||
Returns:
|
||||
如果同时存在Body和Face MESH对象返回True,否则返回False
|
||||
"""
|
||||
return find_body_mesh_impl(children) and find_face_mesh_impl(children)
|
||||
|
||||
|
||||
class TestVRMModelDetectionCorrectness:
|
||||
"""
|
||||
Property 1: VRM Model Detection Correctness
|
||||
*For any* Blender object hierarchy, the VRM detection function SHALL return True
|
||||
if and only if the hierarchy contains an Armature with both "Body" and "Face"
|
||||
mesh children, regardless of the current Blender mode.
|
||||
**Validates: Requirements 1.1, 1.2, 1.3, 1.4**
|
||||
"""
|
||||
|
||||
@given(
|
||||
st.lists(
|
||||
st.tuples(
|
||||
st.text(min_size=1, max_size=20).filter(lambda x: x.strip()),
|
||||
st.sampled_from(["MESH", "ARMATURE", "EMPTY", "LIGHT", "CAMERA"]),
|
||||
),
|
||||
min_size=0,
|
||||
max_size=10,
|
||||
),
|
||||
)
|
||||
@settings(max_examples=100)
|
||||
def test_vrm_detection_requires_both_body_and_face(
|
||||
self,
|
||||
children_specs: list[tuple[str, str]],
|
||||
) -> None:
|
||||
"""
|
||||
Feature: vrm-uv-edge-merger, Property 1: VRM Model Detection Correctness
|
||||
For any set of child objects, VRM detection returns True if and only if
|
||||
both "Body" and "Face" mesh children exist.
|
||||
"""
|
||||
# Check if Body and Face meshes exist
|
||||
has_body_mesh = any(
|
||||
name == "Body" and obj_type == "MESH"
|
||||
for name, obj_type in children_specs
|
||||
)
|
||||
has_face_mesh = any(
|
||||
name == "Face" and obj_type == "MESH"
|
||||
for name, obj_type in children_specs
|
||||
)
|
||||
|
||||
# Test find_body_mesh_impl
|
||||
body_found = find_body_mesh_impl(children_specs)
|
||||
assert body_found == has_body_mesh, (
|
||||
f"find_body_mesh_impl mismatch: expected {has_body_mesh}, "
|
||||
f"got {body_found} for {children_specs}"
|
||||
)
|
||||
|
||||
# Test find_face_mesh_impl
|
||||
face_found = find_face_mesh_impl(children_specs)
|
||||
assert face_found == has_face_mesh, (
|
||||
f"find_face_mesh_impl mismatch: expected {has_face_mesh}, "
|
||||
f"got {face_found} for {children_specs}"
|
||||
)
|
||||
|
||||
# VRM detection should be True only if both exist
|
||||
expected_vrm_valid = has_body_mesh and has_face_mesh
|
||||
actual_vrm_valid = is_valid_vrm_structure(children_specs)
|
||||
assert actual_vrm_valid == expected_vrm_valid, (
|
||||
f"VRM detection mismatch: expected {expected_vrm_valid}, "
|
||||
f"got {actual_vrm_valid} for {children_specs}"
|
||||
)
|
||||
|
||||
def test_vrm_detection_with_valid_vrm_structure(self) -> None:
|
||||
"""
|
||||
Feature: vrm-uv-edge-merger, Property 1: VRM Model Detection Correctness
|
||||
A valid VRM structure with both Body and Face meshes should be detected.
|
||||
"""
|
||||
children = [
|
||||
("Body", "MESH"),
|
||||
("Face", "MESH"),
|
||||
("Hair", "MESH"),
|
||||
]
|
||||
|
||||
assert find_body_mesh_impl(children), "Body mesh should be found"
|
||||
assert find_face_mesh_impl(children), "Face mesh should be found"
|
||||
assert is_valid_vrm_structure(children), "VRM structure should be valid"
|
||||
|
||||
def test_vrm_detection_with_missing_body(self) -> None:
|
||||
"""
|
||||
Feature: vrm-uv-edge-merger, Property 1: VRM Model Detection Correctness
|
||||
Missing Body mesh should result in invalid VRM detection.
|
||||
"""
|
||||
children = [
|
||||
("Face", "MESH"),
|
||||
("Hair", "MESH"),
|
||||
]
|
||||
|
||||
assert not find_body_mesh_impl(children), "Body mesh should not be found"
|
||||
assert find_face_mesh_impl(children), "Face mesh should be found"
|
||||
assert not is_valid_vrm_structure(children), "VRM structure should be invalid"
|
||||
|
||||
def test_vrm_detection_with_missing_face(self) -> None:
|
||||
"""
|
||||
Feature: vrm-uv-edge-merger, Property 1: VRM Model Detection Correctness
|
||||
Missing Face mesh should result in invalid VRM detection.
|
||||
"""
|
||||
children = [
|
||||
("Body", "MESH"),
|
||||
("Hair", "MESH"),
|
||||
]
|
||||
|
||||
assert find_body_mesh_impl(children), "Body mesh should be found"
|
||||
assert not find_face_mesh_impl(children), "Face mesh should not be found"
|
||||
assert not is_valid_vrm_structure(children), "VRM structure should be invalid"
|
||||
|
||||
def test_vrm_detection_with_non_mesh_body(self) -> None:
|
||||
"""
|
||||
Feature: vrm-uv-edge-merger, Property 1: VRM Model Detection Correctness
|
||||
Body object that is not a MESH should not be detected as Body mesh.
|
||||
"""
|
||||
children = [
|
||||
("Body", "EMPTY"), # Not a mesh
|
||||
("Face", "MESH"),
|
||||
]
|
||||
|
||||
assert not find_body_mesh_impl(children), "Non-mesh Body should not be found"
|
||||
assert find_face_mesh_impl(children), "Face mesh should be found"
|
||||
assert not is_valid_vrm_structure(children), "VRM structure should be invalid"
|
||||
|
||||
def test_vrm_detection_with_non_mesh_face(self) -> None:
|
||||
"""
|
||||
Feature: vrm-uv-edge-merger, Property 1: VRM Model Detection Correctness
|
||||
Face object that is not a MESH should not be detected as Face mesh.
|
||||
"""
|
||||
children = [
|
||||
("Body", "MESH"),
|
||||
("Face", "ARMATURE"), # Not a mesh
|
||||
]
|
||||
|
||||
assert find_body_mesh_impl(children), "Body mesh should be found"
|
||||
assert not find_face_mesh_impl(children), "Non-mesh Face should not be found"
|
||||
assert not is_valid_vrm_structure(children), "VRM structure should be invalid"
|
||||
|
||||
def test_vrm_detection_with_empty_children(self) -> None:
|
||||
"""
|
||||
Feature: vrm-uv-edge-merger, Property 1: VRM Model Detection Correctness
|
||||
Empty children list should result in invalid VRM detection.
|
||||
"""
|
||||
children: list[tuple[str, str]] = []
|
||||
|
||||
assert not find_body_mesh_impl(children), "Body should not be found in empty list"
|
||||
assert not find_face_mesh_impl(children), "Face should not be found in empty list"
|
||||
assert not is_valid_vrm_structure(children), "VRM structure should be invalid"
|
||||
|
||||
|
||||
@dataclass
|
||||
class MockOperationState:
|
||||
"""Mock operation state for testing state restoration."""
|
||||
original_mode: str
|
||||
original_active_object: Optional[str] # Object name
|
||||
original_selected_objects: list[str] # Object names
|
||||
processed_materials: list[str]
|
||||
|
||||
|
||||
def save_state_impl(
|
||||
current_mode: str,
|
||||
active_object_name: Optional[str],
|
||||
selected_object_names: list[str],
|
||||
) -> MockOperationState:
|
||||
"""
|
||||
保存当前状态的纯函数实现
|
||||
|
||||
Args:
|
||||
current_mode: 当前Blender模式
|
||||
active_object_name: 当前活动对象名称
|
||||
selected_object_names: 当前选中对象名称列表
|
||||
|
||||
Returns:
|
||||
保存的状态对象
|
||||
"""
|
||||
return MockOperationState(
|
||||
original_mode=current_mode,
|
||||
original_active_object=active_object_name,
|
||||
original_selected_objects=list(selected_object_names),
|
||||
processed_materials=[],
|
||||
)
|
||||
|
||||
|
||||
def restore_state_impl(
|
||||
state: MockOperationState,
|
||||
available_objects: set[str],
|
||||
) -> tuple[str, Optional[str], list[str]]:
|
||||
"""
|
||||
恢复状态的纯函数实现
|
||||
|
||||
Args:
|
||||
state: 保存的状态
|
||||
available_objects: 当前可用的对象名称集合
|
||||
|
||||
Returns:
|
||||
(恢复的模式, 恢复的活动对象, 恢复的选中对象列表)
|
||||
"""
|
||||
# 恢复选中对象(只恢复仍然存在的对象)
|
||||
restored_selected = [
|
||||
name for name in state.original_selected_objects
|
||||
if name in available_objects
|
||||
]
|
||||
|
||||
# 恢复活动对象(只有在对象仍然存在时才恢复)
|
||||
restored_active = None
|
||||
if state.original_active_object and state.original_active_object in available_objects:
|
||||
restored_active = state.original_active_object
|
||||
|
||||
# 恢复模式
|
||||
restored_mode = state.original_mode
|
||||
|
||||
return (restored_mode, restored_active, restored_selected)
|
||||
|
||||
|
||||
class TestStateRestorationOnError:
|
||||
"""
|
||||
Property 7: State Restoration on Error
|
||||
*For any* error during processing, the system SHALL restore the original Blender
|
||||
state (mode, active object, selection) before reporting the error.
|
||||
**Validates: Requirements 7.4**
|
||||
"""
|
||||
|
||||
@given(
|
||||
st.sampled_from(["OBJECT", "EDIT", "SCULPT", "VERTEX_PAINT", "WEIGHT_PAINT"]),
|
||||
st.text(min_size=1, max_size=20).filter(lambda x: x.strip()) | st.none(),
|
||||
st.lists(
|
||||
st.text(min_size=1, max_size=20).filter(lambda x: x.strip()),
|
||||
min_size=0,
|
||||
max_size=5,
|
||||
),
|
||||
)
|
||||
@settings(max_examples=100)
|
||||
def test_state_save_and_restore_property(
|
||||
self,
|
||||
original_mode: str,
|
||||
active_object: Optional[str],
|
||||
selected_objects: list[str],
|
||||
) -> None:
|
||||
"""
|
||||
Feature: vrm-uv-edge-merger, Property 7: State Restoration on Error
|
||||
For any initial state, saving and then restoring should return to the
|
||||
original state (assuming all objects still exist).
|
||||
"""
|
||||
# Save state
|
||||
state = save_state_impl(original_mode, active_object, selected_objects)
|
||||
|
||||
# Verify state was saved correctly
|
||||
assert state.original_mode == original_mode
|
||||
assert state.original_active_object == active_object
|
||||
assert state.original_selected_objects == selected_objects
|
||||
|
||||
# Create available objects set (all objects exist)
|
||||
available_objects = set(selected_objects)
|
||||
if active_object:
|
||||
available_objects.add(active_object)
|
||||
|
||||
# Restore state
|
||||
restored_mode, restored_active, restored_selected = restore_state_impl(
|
||||
state, available_objects
|
||||
)
|
||||
|
||||
# Verify restoration
|
||||
assert restored_mode == original_mode, (
|
||||
f"Mode not restored: expected {original_mode}, got {restored_mode}"
|
||||
)
|
||||
assert restored_active == active_object, (
|
||||
f"Active object not restored: expected {active_object}, got {restored_active}"
|
||||
)
|
||||
assert set(restored_selected) == set(selected_objects), (
|
||||
f"Selected objects not restored: expected {selected_objects}, "
|
||||
f"got {restored_selected}"
|
||||
)
|
||||
|
||||
@given(
|
||||
st.sampled_from(["OBJECT", "EDIT", "SCULPT"]),
|
||||
st.text(min_size=1, max_size=20).filter(lambda x: x.strip()),
|
||||
st.lists(
|
||||
st.text(min_size=1, max_size=20).filter(lambda x: x.strip()),
|
||||
min_size=1,
|
||||
max_size=5,
|
||||
),
|
||||
st.sets(
|
||||
st.text(min_size=1, max_size=20).filter(lambda x: x.strip()),
|
||||
min_size=0,
|
||||
max_size=3,
|
||||
),
|
||||
)
|
||||
@settings(max_examples=100)
|
||||
def test_state_restore_with_deleted_objects_property(
|
||||
self,
|
||||
original_mode: str,
|
||||
active_object: str,
|
||||
selected_objects: list[str],
|
||||
deleted_objects: set[str],
|
||||
) -> None:
|
||||
"""
|
||||
Feature: vrm-uv-edge-merger, Property 7: State Restoration on Error
|
||||
When some objects have been deleted, restoration should only restore
|
||||
objects that still exist.
|
||||
"""
|
||||
# Save state
|
||||
state = save_state_impl(original_mode, active_object, selected_objects)
|
||||
|
||||
# Create available objects (some may have been deleted)
|
||||
all_objects = set(selected_objects)
|
||||
all_objects.add(active_object)
|
||||
available_objects = all_objects - deleted_objects
|
||||
|
||||
# Restore state
|
||||
restored_mode, restored_active, restored_selected = restore_state_impl(
|
||||
state, available_objects
|
||||
)
|
||||
|
||||
# Mode should always be restored
|
||||
assert restored_mode == original_mode
|
||||
|
||||
# Active object should only be restored if it still exists
|
||||
if active_object in deleted_objects:
|
||||
assert restored_active is None, (
|
||||
f"Deleted active object should not be restored: {active_object}"
|
||||
)
|
||||
else:
|
||||
assert restored_active == active_object
|
||||
|
||||
# Selected objects should only include those that still exist
|
||||
for obj_name in restored_selected:
|
||||
assert obj_name in available_objects, (
|
||||
f"Restored selected object {obj_name} should be in available objects"
|
||||
)
|
||||
assert obj_name not in deleted_objects, (
|
||||
f"Deleted object {obj_name} should not be in restored selection"
|
||||
)
|
||||
|
||||
def test_state_restore_with_empty_state(self) -> None:
|
||||
"""
|
||||
Feature: vrm-uv-edge-merger, Property 7: State Restoration on Error
|
||||
Restoring an empty state should work without errors.
|
||||
"""
|
||||
state = save_state_impl("OBJECT", None, [])
|
||||
available_objects: set[str] = set()
|
||||
|
||||
restored_mode, restored_active, restored_selected = restore_state_impl(
|
||||
state, available_objects
|
||||
)
|
||||
|
||||
assert restored_mode == "OBJECT"
|
||||
assert restored_active is None
|
||||
assert restored_selected == []
|
||||
|
||||
def test_state_processed_materials_tracking(self) -> None:
|
||||
"""
|
||||
Feature: vrm-uv-edge-merger, Property 7: State Restoration on Error
|
||||
The state should track processed materials for error reporting.
|
||||
"""
|
||||
state = save_state_impl("OBJECT", "Armature", ["Body", "Face"])
|
||||
|
||||
# Simulate processing materials
|
||||
state.processed_materials.append("Material1")
|
||||
state.processed_materials.append("Material2")
|
||||
|
||||
assert len(state.processed_materials) == 2
|
||||
assert "Material1" in state.processed_materials
|
||||
assert "Material2" in state.processed_materials
|
||||
Reference in New Issue
Block a user