mirror of
https://github.com/deepmodeling/Uni-Lab-OS
synced 2026-03-25 09:09:18 +00:00
feat(app): 模型上传与注册增强 — normalize_model、upload_model_package、backend client
- model_upload.py: normalize_model_package 标准化模型目录 + upload_model_package 上传到后端 - register.py: 设备注册时自动检测并上传本地模型文件 - web/client.py: BackendClient 新增 get_model_upload_urls/publish_model/update_template_model - tests: test_model_upload.py、test_normalize_model.py 单元测试 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
172
tests/app/__init__.py
Normal file
172
tests/app/__init__.py
Normal file
@@ -0,0 +1,172 @@
|
||||
"""normalize_model_for_upload 单元测试"""
|
||||
|
||||
import unittest
|
||||
import sys
|
||||
import os
|
||||
|
||||
# 添加项目根目录到 sys.path
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", ".."))
|
||||
|
||||
from unilabos.app.register import normalize_model_for_upload
|
||||
|
||||
|
||||
class TestNormalizeModelForUpload(unittest.TestCase):
|
||||
"""测试 Registry YAML model 字段标准化"""
|
||||
|
||||
def test_empty_input(self):
|
||||
"""空 dict 直接返回"""
|
||||
self.assertEqual(normalize_model_for_upload({}), {})
|
||||
self.assertIsNone(normalize_model_for_upload(None))
|
||||
|
||||
def test_format_infer_xacro(self):
|
||||
"""自动从 path 后缀推断 format=xacro"""
|
||||
model = {
|
||||
"path": "https://oss.example.com/devices/arm/macro_device.xacro",
|
||||
"mesh": "arm_slider",
|
||||
"type": "device",
|
||||
}
|
||||
result = normalize_model_for_upload(model)
|
||||
self.assertEqual(result["format"], "xacro")
|
||||
|
||||
def test_format_infer_urdf(self):
|
||||
"""自动推断 format=urdf"""
|
||||
model = {"path": "https://example.com/robot.urdf", "type": "device"}
|
||||
result = normalize_model_for_upload(model)
|
||||
self.assertEqual(result["format"], "urdf")
|
||||
|
||||
def test_format_infer_stl(self):
|
||||
"""自动推断 format=stl"""
|
||||
model = {"path": "https://example.com/part.stl"}
|
||||
result = normalize_model_for_upload(model)
|
||||
self.assertEqual(result["format"], "stl")
|
||||
|
||||
def test_format_infer_gltf(self):
|
||||
"""自动推断 format=gltf(.gltf 和 .glb)"""
|
||||
for ext in [".gltf", ".glb"]:
|
||||
model = {"path": f"https://example.com/model{ext}"}
|
||||
result = normalize_model_for_upload(model)
|
||||
self.assertEqual(result["format"], "gltf", f"failed for {ext}")
|
||||
|
||||
def test_format_not_overwritten(self):
|
||||
"""已有 format 字段时不覆盖"""
|
||||
model = {
|
||||
"path": "https://example.com/model.xacro",
|
||||
"format": "custom",
|
||||
}
|
||||
result = normalize_model_for_upload(model)
|
||||
self.assertEqual(result["format"], "custom")
|
||||
|
||||
def test_format_no_path(self):
|
||||
"""没有 path 时不推断 format"""
|
||||
model = {"mesh": "arm_slider", "type": "device"}
|
||||
result = normalize_model_for_upload(model)
|
||||
self.assertNotIn("format", result)
|
||||
|
||||
def test_children_mesh_string_to_struct(self):
|
||||
"""将 children_mesh 字符串(旧格式)转为结构化对象"""
|
||||
model = {
|
||||
"path": "https://example.com/rack.xacro",
|
||||
"type": "resource",
|
||||
"children_mesh": "tip/meshes/tip.stl",
|
||||
"children_mesh_tf": [0.0045, 0.0045, 0, 0, 0, 1.57],
|
||||
"children_mesh_path": "https://oss.example.com/tip.stl",
|
||||
}
|
||||
result = normalize_model_for_upload(model)
|
||||
|
||||
# children_mesh 应变为 dict
|
||||
cm = result["children_mesh"]
|
||||
self.assertIsInstance(cm, dict)
|
||||
self.assertEqual(cm["path"], "https://oss.example.com/tip.stl") # 优先使用 OSS URL
|
||||
self.assertEqual(cm["format"], "stl")
|
||||
self.assertTrue(cm["default_visible"])
|
||||
self.assertEqual(cm["local_offset"], [0.0045, 0.0045, 0])
|
||||
self.assertEqual(cm["local_rotation"], [0, 0, 1.57])
|
||||
|
||||
# 旧字段应被移除
|
||||
self.assertNotIn("children_mesh_tf", result)
|
||||
self.assertNotIn("children_mesh_path", result)
|
||||
|
||||
def test_children_mesh_no_oss_fallback(self):
|
||||
"""children_mesh 无 OSS URL 时 fallback 到本地路径"""
|
||||
model = {
|
||||
"path": "https://example.com/rack.xacro",
|
||||
"children_mesh": "plate_96/meshes/plate_96.stl",
|
||||
}
|
||||
result = normalize_model_for_upload(model)
|
||||
cm = result["children_mesh"]
|
||||
self.assertEqual(cm["path"], "plate_96/meshes/plate_96.stl")
|
||||
self.assertEqual(cm["format"], "stl")
|
||||
|
||||
def test_children_mesh_gltf_format(self):
|
||||
"""children_mesh .glb 文件推断 format=gltf"""
|
||||
model = {
|
||||
"path": "https://example.com/rack.xacro",
|
||||
"children_mesh": "meshes/child.glb",
|
||||
}
|
||||
result = normalize_model_for_upload(model)
|
||||
self.assertEqual(result["children_mesh"]["format"], "gltf")
|
||||
|
||||
def test_children_mesh_partial_tf(self):
|
||||
"""children_mesh_tf 只有 3 个值时只有 offset 无 rotation"""
|
||||
model = {
|
||||
"path": "https://example.com/rack.xacro",
|
||||
"children_mesh": "tip.stl",
|
||||
"children_mesh_tf": [0.01, 0.02, 0.03],
|
||||
}
|
||||
result = normalize_model_for_upload(model)
|
||||
cm = result["children_mesh"]
|
||||
self.assertEqual(cm["local_offset"], [0.01, 0.02, 0.03])
|
||||
self.assertNotIn("local_rotation", cm)
|
||||
|
||||
def test_children_mesh_no_tf(self):
|
||||
"""children_mesh 无 tf 时不加 offset/rotation"""
|
||||
model = {
|
||||
"path": "https://example.com/rack.xacro",
|
||||
"children_mesh": "tip.stl",
|
||||
}
|
||||
result = normalize_model_for_upload(model)
|
||||
cm = result["children_mesh"]
|
||||
self.assertNotIn("local_offset", cm)
|
||||
self.assertNotIn("local_rotation", cm)
|
||||
|
||||
def test_children_mesh_already_dict(self):
|
||||
"""children_mesh 已经是 dict 时不重新映射"""
|
||||
model = {
|
||||
"path": "https://example.com/rack.xacro",
|
||||
"children_mesh": {
|
||||
"path": "https://example.com/tip.stl",
|
||||
"format": "stl",
|
||||
"default_visible": False,
|
||||
},
|
||||
}
|
||||
result = normalize_model_for_upload(model)
|
||||
cm = result["children_mesh"]
|
||||
self.assertIsInstance(cm, dict)
|
||||
self.assertFalse(cm["default_visible"])
|
||||
|
||||
def test_original_not_mutated(self):
|
||||
"""原始 dict 不被修改"""
|
||||
original = {
|
||||
"path": "https://example.com/model.xacro",
|
||||
"mesh": "arm",
|
||||
}
|
||||
original_copy = {**original}
|
||||
normalize_model_for_upload(original)
|
||||
self.assertEqual(original, original_copy)
|
||||
|
||||
def test_preserves_existing_fields(self):
|
||||
"""所有原始字段都被保留"""
|
||||
model = {
|
||||
"path": "https://example.com/model.xacro",
|
||||
"mesh": "arm_slider",
|
||||
"type": "device",
|
||||
"mesh_tf": [0, 0, 0, 0, 0, 0],
|
||||
"custom_field": "should_survive",
|
||||
}
|
||||
result = normalize_model_for_upload(model)
|
||||
self.assertEqual(result["custom_field"], "should_survive")
|
||||
self.assertEqual(result["mesh_tf"], [0, 0, 0, 0, 0, 0])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user