init version

This commit is contained in:
Xuwznln
2025-09-02 16:39:44 +08:00
commit 94f0c112e5
41 changed files with 6004 additions and 0 deletions

View File

@@ -0,0 +1,463 @@
import os
import sys
import pytest
# 添加项目路径
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
# JSON Schema 依赖检查
from msgcenterpy.core.type_info import ConstraintType
from msgcenterpy.core.types import MessageType
from msgcenterpy.instances.json_schema_instance import JSONSchemaMessageInstance
class TestJSONSchemaMessageInstance:
"""JSONSchemaMessageInstance 基本功能测试"""
@pytest.fixture
def simple_schema(self):
"""简单的 JSON Schema 示例"""
return {
"type": "object",
"properties": {
"name": {"type": "string"},
"age": {"type": "integer", "minimum": 0, "maximum": 150},
"active": {"type": "boolean"},
},
"required": ["name"],
}
@pytest.fixture
def complex_schema(self):
"""复杂的 JSON Schema 示例"""
return {
"type": "object",
"properties": {
"user": {
"type": "object",
"properties": {
"id": {"type": "string"},
"profile": {
"type": "object",
"properties": {
"email": {"type": "string", "format": "email"},
"phone": {
"type": "string",
"pattern": "^\\+?[1-9]\\d{1,14}$",
},
},
},
},
"required": ["id"],
},
"tags": {
"type": "array",
"items": {"type": "string"},
"minItems": 1,
"maxItems": 10,
},
"scores": {
"type": "array",
"items": {"type": "number", "minimum": 0, "maximum": 100},
},
"metadata": {"type": "object", "additionalProperties": True},
},
"required": ["user", "tags"],
}
@pytest.fixture
def simple_data(self):
"""匹配简单 schema 的数据"""
return {"name": "John Doe", "age": 30, "active": True}
@pytest.fixture
def complex_data(self):
"""匹配复杂 schema 的数据"""
return {
"user": {
"id": "user_123",
"profile": {"email": "john@example.com", "phone": "+1234567890"},
},
"tags": ["developer", "python", "testing"],
"scores": [85.5, 92.0, 78.3],
"metadata": {"created_at": "2024-01-01", "version": 1},
}
def test_basic_creation(self, simple_schema, simple_data):
"""测试基本创建功能"""
instance = JSONSchemaMessageInstance(simple_data, simple_schema)
assert instance.message_type == MessageType.JSON_SCHEMA
assert instance.inner_data == simple_data
assert instance.json_schema == simple_schema
assert len(instance._validation_errors) == 0
def test_data_validation_success(self, simple_schema, simple_data):
"""测试数据验证成功"""
instance = JSONSchemaMessageInstance(simple_data, simple_schema)
# 验证成功,没有错误
assert len(instance._validation_errors) == 0
def test_data_validation_failure(self, simple_schema):
"""测试数据验证失败"""
invalid_data = {
"age": -5, # 违反 minimum 约束
"active": "not_boolean", # 类型错误
# 缺少必需的 "name" 字段
}
instance = JSONSchemaMessageInstance(invalid_data, simple_schema)
# 应该有验证错误
assert len(instance._validation_errors) > 0
def test_get_python_dict(self, simple_schema, simple_data):
"""测试获取 Python 字典"""
instance = JSONSchemaMessageInstance(simple_data, simple_schema)
result = instance.get_python_dict()
assert isinstance(result, dict)
assert result == simple_data
assert result is not simple_data # 应该是副本
def test_set_python_dict(self, simple_schema, simple_data):
"""测试设置 Python 字典"""
instance = JSONSchemaMessageInstance(simple_data, simple_schema)
new_data = {"name": "Jane Smith", "age": 25}
result = instance.set_python_dict(new_data)
assert result is True
assert instance.get_python_dict()["name"] == "Jane Smith"
assert instance.get_python_dict()["age"] == 25
def test_export_to_envelope(self, simple_schema, simple_data):
"""测试导出信封"""
instance = JSONSchemaMessageInstance(simple_data, simple_schema)
envelope = instance.export_to_envelope()
assert "content" in envelope
assert "metadata" in envelope
assert envelope["content"] == simple_data
metadata = envelope["metadata"]
assert metadata["current_format"] == "json_schema"
assert "properties" in metadata
def test_import_from_envelope(self, simple_schema, simple_data):
"""测试从信封导入"""
# 创建原始实例
original = JSONSchemaMessageInstance(simple_data, simple_schema)
envelope = original.export_to_envelope()
# 从信封导入新实例
new_instance = JSONSchemaMessageInstance.import_from_envelope(envelope)
assert isinstance(new_instance, JSONSchemaMessageInstance)
assert new_instance.get_python_dict() == simple_data
assert new_instance.json_schema == simple_schema
class TestJSONSchemaFieldTypeInfo:
"""JSON Schema 字段类型信息测试"""
@pytest.fixture
def typed_schema(self):
"""包含各种类型的 schema"""
return {
"type": "object",
"properties": {
"string_field": {"type": "string", "minLength": 3, "maxLength": 50},
"integer_field": {"type": "integer", "minimum": 0, "maximum": 100},
"number_field": {"type": "number", "multipleOf": 0.5},
"boolean_field": {"type": "boolean"},
"array_field": {
"type": "array",
"items": {"type": "string"},
"minItems": 1,
"maxItems": 5,
},
"object_field": {
"type": "object",
"properties": {"nested_string": {"type": "string"}},
},
"enum_field": {
"type": "string",
"enum": ["option1", "option2", "option3"],
},
"format_field": {"type": "string", "format": "email"},
},
"required": ["string_field", "integer_field"],
}
@pytest.fixture
def typed_data(self):
"""匹配类型化 schema 的数据"""
return {
"string_field": "hello",
"integer_field": 42,
"number_field": 3.5,
"boolean_field": True,
"array_field": ["item1", "item2"],
"object_field": {"nested_string": "nested_value"},
"enum_field": "option1",
"format_field": "test@example.com",
}
def test_string_field_type_info(self, typed_schema, typed_data):
"""测试字符串字段类型信息"""
instance = JSONSchemaMessageInstance(typed_data, typed_schema)
type_info = instance.fields.get_sub_type_info("string_field")
assert type_info is not None
assert type_info.field_name == "string_field"
assert type_info.standard_type.value == "string"
assert type_info.current_value == "hello"
# 检查约束
assert type_info.has_constraint(ConstraintType.MIN_LENGTH)
assert type_info.get_constraint_value(ConstraintType.MIN_LENGTH) == 3
assert type_info.has_constraint(ConstraintType.MAX_LENGTH)
assert type_info.get_constraint_value(ConstraintType.MAX_LENGTH) == 50
assert type_info.has_constraint(ConstraintType.REQUIRED)
def test_integer_field_type_info(self, typed_schema, typed_data):
"""测试整数字段类型信息"""
instance = JSONSchemaMessageInstance(typed_data, typed_schema)
type_info = instance.fields.get_sub_type_info("integer_field")
assert type_info is not None
assert type_info.standard_type.value == "integer"
assert type_info.current_value == 42
# 检查数值约束
assert type_info.has_constraint(ConstraintType.MIN_VALUE)
assert type_info.get_constraint_value(ConstraintType.MIN_VALUE) == 0
assert type_info.has_constraint(ConstraintType.MAX_VALUE)
assert type_info.get_constraint_value(ConstraintType.MAX_VALUE) == 100
assert type_info.has_constraint(ConstraintType.REQUIRED)
def test_array_field_type_info(self, typed_schema, typed_data):
"""测试数组字段类型信息"""
instance = JSONSchemaMessageInstance(typed_data, typed_schema)
type_info = instance.fields.get_sub_type_info("array_field")
assert type_info is not None
assert type_info.is_array is True
assert type_info.current_value == ["item1", "item2"]
# 检查数组约束
assert type_info.has_constraint(ConstraintType.MIN_ITEMS)
assert type_info.get_constraint_value(ConstraintType.MIN_ITEMS) == 1
assert type_info.has_constraint(ConstraintType.MAX_ITEMS)
assert type_info.get_constraint_value(ConstraintType.MAX_ITEMS) == 5
# 检查元素类型信息
assert type_info.element_type_info is not None
assert type_info.element_type_info.standard_type.value == "string"
def test_object_field_type_info(self, typed_schema, typed_data):
"""测试对象字段类型信息"""
instance = JSONSchemaMessageInstance(typed_data, typed_schema)
type_info = instance.fields.get_sub_type_info("object_field")
assert type_info is not None
assert type_info.is_object is True
assert type_info.current_value == {"nested_string": "nested_value"}
# 检查对象字段定义
assert len(type_info.object_fields) > 0
assert "nested_string" in type_info.object_fields
nested_field_info = type_info.object_fields["nested_string"]
assert nested_field_info.standard_type.value == "string"
def test_enum_field_type_info(self, typed_schema, typed_data):
"""测试枚举字段类型信息"""
instance = JSONSchemaMessageInstance(typed_data, typed_schema)
type_info = instance.fields.get_sub_type_info("enum_field")
assert type_info is not None
assert type_info.has_constraint(ConstraintType.ENUM_VALUES)
enum_values = type_info.get_constraint_value(ConstraintType.ENUM_VALUES)
assert enum_values == ["option1", "option2", "option3"]
def test_format_field_type_info(self, typed_schema, typed_data):
"""测试格式字段类型信息"""
instance = JSONSchemaMessageInstance(typed_data, typed_schema)
type_info = instance.fields.get_sub_type_info("format_field")
assert type_info is not None
assert type_info.has_constraint(ConstraintType.FORMAT)
format_value = type_info.get_constraint_value(ConstraintType.FORMAT)
assert format_value == "email"
class TestJSONSchemaInstanceJSONSchema:
"""JSONSchemaMessageInstance 自身的 JSON Schema 生成测试"""
def test_get_json_schema_simple(self):
"""测试简单数据的 JSON Schema 生成"""
schema = {
"type": "object",
"properties": {"name": {"type": "string"}, "count": {"type": "integer"}},
}
data = {"name": "test", "count": 5}
instance = JSONSchemaMessageInstance(data, schema)
generated_schema = instance.get_json_schema()
assert generated_schema["type"] == "object"
assert "properties" in generated_schema
assert "name" in generated_schema["properties"]
assert "count" in generated_schema["properties"]
assert generated_schema["title"] == "JSONSchemaMessageInstance Schema"
def test_get_json_schema_with_constraints(self):
"""测试包含约束的 JSON Schema 生成"""
schema = {
"type": "object",
"properties": {
"email": {"type": "string", "format": "email", "minLength": 5},
"age": {"type": "integer", "minimum": 0, "maximum": 120},
"tags": {"type": "array", "items": {"type": "string"}, "minItems": 1},
},
"required": ["email"],
}
data = {"email": "test@example.com", "age": 25, "tags": ["tag1", "tag2"]}
instance = JSONSchemaMessageInstance(data, schema)
generated_schema = instance.get_json_schema()
# 检查约束是否保留在生成的 schema 中
properties = generated_schema["properties"]
# email 字段约束
email_prop = properties["email"]
assert email_prop["type"] == "string"
# age 字段约束
age_prop = properties["age"]
assert age_prop["type"] == "integer"
# tags 数组约束
tags_prop = properties["tags"]
assert tags_prop["type"] == "array"
def test_get_json_schema_nested_objects(self):
"""测试嵌套对象的 JSON Schema 生成"""
schema = {
"type": "object",
"properties": {
"user": {
"type": "object",
"properties": {
"id": {"type": "string"},
"settings": {
"type": "object",
"properties": {"theme": {"type": "string"}},
},
},
}
},
}
data = {"user": {"id": "user123", "settings": {"theme": "dark"}}}
instance = JSONSchemaMessageInstance(data, schema)
generated_schema = instance.get_json_schema()
assert "user" in generated_schema["properties"]
user_prop = generated_schema["properties"]["user"]
assert user_prop["type"] == "object"
class TestJSONSchemaValidation:
"""JSON Schema 验证功能测试"""
def test_constraint_validation(self):
"""测试约束验证"""
schema = {
"type": "object",
"properties": {
"age": {"type": "integer", "minimum": 0, "maximum": 150},
"name": {"type": "string", "minLength": 2, "maxLength": 50},
},
"required": ["name"],
}
# 有效数据
valid_data = {"name": "John", "age": 30}
valid_instance = JSONSchemaMessageInstance(valid_data, schema)
assert len(valid_instance._validation_errors) == 0
# 无效数据 - 年龄超出范围
invalid_data1 = {"name": "John", "age": 200}
invalid_instance1 = JSONSchemaMessageInstance(invalid_data1, schema)
assert len(invalid_instance1._validation_errors) > 0
# 无效数据 - 缺少必需字段
invalid_data2 = {"age": 30}
invalid_instance2 = JSONSchemaMessageInstance(invalid_data2, schema)
assert len(invalid_instance2._validation_errors) > 0
def test_type_validation(self):
"""测试类型验证"""
schema = {
"type": "object",
"properties": {
"count": {"type": "integer"},
"active": {"type": "boolean"},
"items": {"type": "array", "items": {"type": "string"}},
},
}
# 类型正确的数据
valid_data = {"count": 42, "active": True, "items": ["a", "b", "c"]}
valid_instance = JSONSchemaMessageInstance(valid_data, schema)
assert len(valid_instance._validation_errors) == 0
# 类型错误的数据
invalid_data = {
"count": "not_integer",
"active": "not_boolean",
"items": "not_array",
}
invalid_instance = JSONSchemaMessageInstance(invalid_data, schema)
assert len(invalid_instance._validation_errors) > 0
# 运行测试的便捷函数
def run_json_schema_tests():
"""运行 JSON Schema 相关测试"""
import subprocess
result = subprocess.run(
[
"python",
"-m",
"pytest",
"tests/test_json_schema_instance.py",
"-v",
"--tb=short",
],
capture_output=True,
text=True,
)
print(result.stdout)
if result.stderr:
print("STDERR:", result.stderr)
return result.returncode == 0
if __name__ == "__main__":
# 直接运行测试
run_json_schema_tests()

325
tests/test_ros2_instance.py Normal file
View File

@@ -0,0 +1,325 @@
import array
import os
import sys
import pytest
from msgcenterpy import TypeConverter
# Add project path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
# ROS2 dependency check
try:
from geometry_msgs.msg import Point, Pose
from std_msgs.msg import Float64MultiArray, String
# Only import ROS2MessageInstance when ROS2 message packages are available
from msgcenterpy.instances.ros2_instance import ROS2MessageInstance
HAS_ROS2 = True
except ImportError:
HAS_ROS2 = False
from msgcenterpy.core.types import MessageType
class TestROS2MessageInstance:
"""ROS2MessageInstance test class"""
@pytest.mark.skipif(not HAS_ROS2, reason="ROS2 dependencies not available")
def test_basic_creation_string_message(self):
"""Test basic String message creation"""
# Create String message
string_msg = String()
string_msg.data = "Hello ROS2"
# Create ROS2MessageInstance
ros2_inst = ROS2MessageInstance(string_msg)
assert ros2_inst.message_type == MessageType.ROS2
assert ros2_inst.inner_data is string_msg
assert ros2_inst.ros_msg_cls == String
assert ros2_inst.ros_msg_cls_namespace == "std_msgs/msg/String"
@pytest.mark.skipif(not HAS_ROS2, reason="ROS2 dependencies not available")
def test_basic_creation_float_array(self):
"""Test Float64MultiArray message creation"""
# Create Float64MultiArray message
array_msg = Float64MultiArray()
array_msg.data = [1.1, 2.2, 3.3, 4.4, 5.5]
# Create ROS2MessageInstance
ros2_inst = ROS2MessageInstance(array_msg)
assert ros2_inst.message_type == MessageType.ROS2
assert ros2_inst.inner_data is array_msg
assert ros2_inst.ros_msg_cls == Float64MultiArray
@pytest.mark.skipif(not HAS_ROS2, reason="ROS2 dependencies not available")
def test_simple_field_assignment(self):
"""Test simple field assignment - based on __main__ test1"""
# Create String message
string_msg = String()
ros2_inst = ROS2MessageInstance(string_msg)
# Initial state check
assert string_msg.data == ""
# Test field assignment
ros2_inst.data = "test_value" # Assignment through field accessor
assert string_msg.data == "test_value"
assert ros2_inst.inner_data.data == "test_value"
@pytest.mark.skipif(not HAS_ROS2, reason="ROS2 dependencies not available")
def test_nested_field_assignment(self):
"""测试嵌套字段赋值 - 基于 __main__ 测试2,3"""
# Create Pose message
pose_msg = Pose()
ros2_inst = ROS2MessageInstance(pose_msg)
# 测试嵌套字段赋值
ros2_inst.position.x = 1.5
ros2_inst.position.y = 2.5
ros2_inst.position.z = 3.5
assert pose_msg.position.x == 1.5
assert pose_msg.position.y == 2.5
assert pose_msg.position.z == 3.5
# 测试整个对象赋值
new_position = Point(x=10.0, y=20.0, z=30.0)
ros2_inst.position = new_position
assert pose_msg.position.x == 10.0
assert pose_msg.position.y == 20.0
assert pose_msg.position.z == 30.0
@pytest.mark.skipif(not HAS_ROS2, reason="ROS2 dependencies not available")
def test_export_to_envelope(self):
"""测试导出信封功能 - 基于 __main__ 测试6"""
# Create and setup String message
string_msg = String()
string_msg.data = "test_envelope_data"
ros2_inst = ROS2MessageInstance(string_msg)
# 导出信封
envelope = ros2_inst.export_to_envelope()
# 验证信封结构
assert "content" in envelope
assert "metadata" in envelope
assert envelope["content"]["data"] == "test_envelope_data"
# 验证元数据
metadata = envelope["metadata"]
assert metadata["current_format"] == "ros2"
assert "properties" in metadata
properties = metadata["properties"]
assert "ros_msg_cls_namespace" in properties
assert "ros_msg_cls_path" in properties
@pytest.mark.skipif(not HAS_ROS2, reason="ROS2 dependencies not available")
def test_get_python_dict(self):
"""测试获取 Python 字典"""
# Create Float64MultiArray message
array_msg = Float64MultiArray()
array_msg.data = [1.0, 2.0, 3.0]
ros2_inst = ROS2MessageInstance(array_msg)
# 获取 Python 字典
python_dict = ros2_inst.get_python_dict()
assert isinstance(python_dict, dict)
assert "data" in python_dict
assert python_dict["data"] == [1.0, 2.0, 3.0]
@pytest.mark.skipif(not HAS_ROS2, reason="ROS2 dependencies not available")
def test_set_python_dict(self):
"""测试设置 Python 字典"""
# Create String message
string_msg = String()
ros2_inst = ROS2MessageInstance(string_msg)
# 设置字典数据
new_data = {"data": "updated_value"}
result = ros2_inst.set_python_dict(new_data)
assert result is True
assert string_msg.data == "updated_value"
@pytest.mark.skipif(not HAS_ROS2, reason="ROS2 dependencies not available")
def test_field_type_info_extraction(self):
"""测试字段类型信息提取"""
# Create Float64MultiArray message
array_msg = Float64MultiArray()
array_msg.data = [1.0, 2.0, 3.0]
ros2_inst = ROS2MessageInstance(array_msg)
# 获取字段类型信息
type_info = ros2_inst.fields.get_sub_type_info("data")
assert type_info is not None
assert type_info.field_name == "data"
assert type_info.is_array is True
assert type_info.python_type == array.array
assert type_info.python_value_from_standard_type == [1.0, 2.0, 3.0]
@pytest.mark.skipif(not HAS_ROS2, reason="ROS2 dependencies not available")
def test_obtain_ros_cls_from_string(self):
"""测试从字符串获取 ROS 类"""
# 测试 namespace 格式
ros_cls_ns = ROS2MessageInstance.obtain_ros_cls_from_str("std_msgs/msg/String")
assert ros_cls_ns == String
# 测试模块路径格式
ros_cls_path = ROS2MessageInstance.obtain_ros_cls_from_str("std_msgs.msg._string.String")
assert ros_cls_path == String
# 测试直接传入类
ros_cls_direct = ROS2MessageInstance.obtain_ros_cls_from_str(String)
assert ros_cls_direct == String
@pytest.mark.skipif(not HAS_ROS2, reason="ROS2 dependencies not available")
def test_ros_msg_cls_properties(self):
"""测试 ROS 消息类属性"""
string_msg = String()
ros2_inst = ROS2MessageInstance(string_msg)
# 测试类路径属性
cls_path = ros2_inst.ros_msg_cls_path
assert "std_msgs.msg" in cls_path
assert "String" in cls_path
# 测试命名空间属性
namespace = ros2_inst.ros_msg_cls_namespace
assert namespace == "std_msgs/msg/String"
@pytest.mark.skipif(not HAS_ROS2, reason="ROS2 dependencies not available")
def test_import_from_envelope(self):
"""测试从信封导入"""
# Create original message
original_msg = String()
original_msg.data = "envelope_test"
original_inst = ROS2MessageInstance(original_msg)
# 导出信封
envelope = original_inst.export_to_envelope()
# 从信封导入新实例
new_inst = ROS2MessageInstance.import_from_envelope(envelope)
assert isinstance(new_inst, ROS2MessageInstance)
assert new_inst.inner_data.data == "envelope_test"
assert new_inst.ros_msg_cls == String
class TestROS2MessageInstanceJSONSchema:
"""ROS2MessageInstance JSON Schema 生成测试"""
@pytest.mark.skipif(not HAS_ROS2, reason="ROS2 dependencies not available")
def test_get_json_schema_string_message(self):
"""测试 String 消息的 JSON Schema 生成"""
string_msg = String()
string_msg.data = "test_schema"
ros2_inst = ROS2MessageInstance(string_msg)
# 生成 JSON Schema
schema = ros2_inst.get_json_schema()
assert schema["type"] == "object"
assert "properties" in schema
assert "data" in schema["properties"]
assert schema["properties"]["data"]["type"] == "string"
assert schema["title"] == "ROS2MessageInstance Schema"
@pytest.mark.skipif(not HAS_ROS2, reason="ROS2 dependencies not available")
def test_get_json_schema_float_array(self):
"""测试 Float64MultiArray 的 JSON Schema 生成"""
array_msg = Float64MultiArray()
array_msg.data = [1.1, 2.2, 3.3]
ros2_inst = ROS2MessageInstance(array_msg)
# 生成 JSON Schema
schema = ros2_inst.get_json_schema()
assert schema["type"] == "object"
assert "properties" in schema
assert "data" in schema["properties"]
# 检查数组类型
data_prop = schema["properties"]["data"]
assert data_prop["type"] == "array"
assert "items" in data_prop
assert data_prop["items"]["type"] == "number"
@pytest.mark.skipif(not HAS_ROS2, reason="ROS2 dependencies not available")
def test_get_json_schema_pose_message(self):
"""测试复杂 Pose 消息的 JSON Schema 生成"""
pose_msg = Pose()
pose_msg.position.x = 1.0
pose_msg.position.y = 2.0
pose_msg.position.z = 3.0
ros2_inst = ROS2MessageInstance(pose_msg)
# 生成 JSON Schema
schema = ros2_inst.get_json_schema()
assert schema["type"] == "object"
assert "properties" in schema
# 检查嵌套对象
properties = schema["properties"]
assert "position" in properties
assert "orientation" in properties
# 验证对象类型
position_prop = properties["position"]
assert position_prop["type"] == "object"
@pytest.mark.skipif(not HAS_ROS2, reason="ROS2 dependencies not available")
def test_json_schema_constraint_extraction(self):
"""测试约束条件提取"""
array_msg = Float64MultiArray()
array_msg.data = [1.0, 2.0, 3.0, 4.0, 5.0]
ros2_inst = ROS2MessageInstance(array_msg)
# 获取字段类型信息检查约束
type_info = ros2_inst.fields.get_sub_type_info("data")
assert type_info is not None
assert type_info.is_array is True
# 生成 Schema 并检查约束是否转换
schema = ros2_inst.get_json_schema()
data_prop = schema["properties"]["data"]
assert data_prop["type"] == "array"
# 运行测试的便捷函数
def run_ros2_tests():
"""运行 ROS2 相关测试"""
if not HAS_ROS2:
print("❌ ROS2 dependencies not available, skipping tests")
return False
import subprocess
result = subprocess.run(
["python", "-m", "pytest", "tests/test_ros2_instance.py", "-v", "--tb=short"],
capture_output=True,
text=True,
)
print(result.stdout)
if result.stderr:
print("STDERR:", result.stderr)
return result.returncode == 0
if __name__ == "__main__":
# 直接运行测试
run_ros2_tests()

View File

@@ -0,0 +1,384 @@
import os
import sys
import pytest
# 添加项目路径
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
# 依赖检查
try:
from geometry_msgs.msg import Point, Pose, Vector3
from std_msgs.msg import Bool, Float64MultiArray, Int32, String
# 只有在 ROS2 消息包可用时才导入 ROS2MessageInstance
from msgcenterpy.instances.ros2_instance import ROS2MessageInstance
HAS_ROS2 = True
except ImportError:
HAS_ROS2 = False
import jsonschema
from msgcenterpy.core.types import MessageType
from msgcenterpy.instances.json_schema_instance import JSONSchemaMessageInstance
class TestROS2ToJSONSchemaConversion:
"""ROS2 转 JSON Schema 转换测试"""
@pytest.mark.skipif(not HAS_ROS2, reason="Missing dependencies")
def test_string_message_to_json_schema(self):
"""测试 String 消息转 JSON Schema"""
# 创建 ROS2 String 消息
string_msg = String()
string_msg.data = "Hello JSON Schema"
ros2_inst = ROS2MessageInstance(string_msg)
# 生成 JSON Schema
schema = ros2_inst.get_json_schema()
# 验证 Schema 结构
assert schema["type"] == "object"
assert "properties" in schema
assert "data" in schema["properties"]
assert schema["properties"]["data"]["type"] == "string"
assert schema["title"] == "ROS2MessageInstance Schema"
assert "ros2" in schema["description"]
# 验证与原始数据的一致性
ros2_dict = ros2_inst.get_python_dict()
assert "data" in ros2_dict
assert ros2_dict["data"] == "Hello JSON Schema"
@pytest.mark.skipif(not HAS_ROS2, reason="Missing dependencies")
def test_float_array_to_json_schema(self):
"""测试 Float64MultiArray 转 JSON Schema"""
# 创建 Float64MultiArray 消息
array_msg = Float64MultiArray()
array_msg.data = [1.1, 2.2, 3.3, 4.4, 5.5]
ros2_inst = ROS2MessageInstance(array_msg)
# 生成 JSON Schema
schema = ros2_inst.get_json_schema()
# 验证数组字段的 Schema
assert "data" in schema["properties"]
data_prop = schema["properties"]["data"]
assert data_prop["type"] == "array"
assert "items" in data_prop
assert data_prop["items"]["type"] == "number"
# 验证约束条件
assert "minItems" in data_prop or "maxItems" in data_prop or data_prop["type"] == "array"
@pytest.mark.skipif(not HAS_ROS2, reason="Missing dependencies")
def test_pose_message_to_json_schema(self):
"""测试复杂 Pose 消息转 JSON Schema"""
# 创建 Pose 消息
pose_msg = Pose()
pose_msg.position.x = 1.0
pose_msg.position.y = 2.0
pose_msg.position.z = 3.0
pose_msg.orientation.w = 1.0
ros2_inst = ROS2MessageInstance(pose_msg)
# 生成 JSON Schema
schema = ros2_inst.get_json_schema()
# 验证嵌套对象结构
properties = schema["properties"]
assert "position" in properties
assert "orientation" in properties
# 验证对象类型
position_prop = properties["position"]
assert position_prop["type"] == "object"
orientation_prop = properties["orientation"]
assert orientation_prop["type"] == "object"
class TestROS2ToJSONSchemaInstanceConversion:
"""ROS2 转 JSONSchemaMessageInstance 转换测试"""
@pytest.mark.skipif(not HAS_ROS2, reason="Missing dependencies")
def test_ros2_to_json_schema_instance_string(self):
"""测试 ROS2 String 转 JSONSchemaMessageInstance"""
# 创建 ROS2 实例
string_msg = String()
string_msg.data = "Test conversion"
ros2_inst = ROS2MessageInstance(string_msg)
# 转换为 JSONSchemaMessageInstance
json_schema_inst = ros2_inst.to_json_schema()
# 验证转换结果
assert isinstance(json_schema_inst, JSONSchemaMessageInstance)
assert json_schema_inst.message_type == MessageType.JSON_SCHEMA
# 验证数据一致性
original_data = ros2_inst.get_python_dict()
converted_data = json_schema_inst.get_python_dict()
assert original_data == converted_data
# 验证 Schema 存在
schema = json_schema_inst.json_schema
assert schema is not None
assert schema["type"] == "object"
assert "properties" in schema
@pytest.mark.skipif(not HAS_ROS2, reason="Missing dependencies")
def test_ros2_to_json_schema_instance_array(self):
"""测试 ROS2 数组转 JSONSchemaMessageInstance"""
# 创建 ROS2 数组实例
array_msg = Float64MultiArray()
array_msg.data = [10.5, 20.3, 30.7]
ros2_inst = ROS2MessageInstance(array_msg)
# 转换为 JSONSchemaMessageInstance
json_schema_inst = ros2_inst.to_json_schema()
# 验证转换结果
assert isinstance(json_schema_inst, JSONSchemaMessageInstance)
# 验证数据一致性
original_data = ros2_inst.get_python_dict()
converted_data = json_schema_inst.get_python_dict()
assert original_data == converted_data
assert converted_data["data"] == [10.5, 20.3, 30.7]
# 验证 Schema 的数组类型
schema = json_schema_inst.json_schema
if "data" in schema["properties"]:
data_prop = schema["properties"]["data"]
assert data_prop["type"] == "array"
@pytest.mark.skipif(not HAS_ROS2, reason="Missing dependencies")
def test_ros2_to_json_schema_instance_pose(self):
"""测试 ROS2 Pose 转 JSONSchemaMessageInstance"""
# 创建复杂的 Pose 消息
pose_msg = Pose()
pose_msg.position.x = 5.0
pose_msg.position.y = 10.0
pose_msg.position.z = 15.0
pose_msg.orientation.x = 0.0
pose_msg.orientation.y = 0.0
pose_msg.orientation.z = 0.0
pose_msg.orientation.w = 1.0
ros2_inst = ROS2MessageInstance(pose_msg)
# 转换为 JSONSchemaMessageInstance
json_schema_inst = ros2_inst.to_json_schema()
# 验证转换结果
assert isinstance(json_schema_inst, JSONSchemaMessageInstance)
# 验证嵌套数据一致性
original_data = ros2_inst.get_python_dict()
converted_data = json_schema_inst.get_python_dict()
assert original_data == converted_data
# 验证嵌套结构
assert "position" in converted_data
assert "orientation" in converted_data
assert converted_data["position"]["x"] == 5.0
assert converted_data["position"]["y"] == 10.0
assert converted_data["position"]["z"] == 15.0
class TestConversionConsistency:
"""转换一致性测试"""
@pytest.mark.skipif(not HAS_ROS2, reason="Missing dependencies")
def test_schema_data_consistency(self):
"""测试 Schema 与数据的一致性"""
# 创建多种类型的数据
test_cases = []
# String 消息
string_msg = String()
string_msg.data = "consistency_test"
test_cases.append(("String", ROS2MessageInstance(string_msg)))
# Float64MultiArray 消息
array_msg = Float64MultiArray()
array_msg.data = [1.0, 2.0, 3.0]
test_cases.append(("Float64MultiArray", ROS2MessageInstance(array_msg)))
for test_name, ros2_inst in test_cases:
# 生成 Schema
schema = ros2_inst.get_json_schema()
original_data = ros2_inst.get_python_dict()
# 验证 Schema 属性与数据字段一致
schema_props = set(schema["properties"].keys())
data_keys = set(original_data.keys())
assert schema_props == data_keys, f"{test_name}: Schema properties and data keys don't match"
# 验证每个字段的类型一致性
for field_name, field_value in original_data.items():
if field_name in schema["properties"]:
prop_schema = schema["properties"][field_name]
# 基本类型检查
if isinstance(field_value, str):
assert prop_schema["type"] == "string", f"{test_name}.{field_name}: Type mismatch"
elif isinstance(field_value, bool):
assert prop_schema["type"] == "boolean", f"{test_name}.{field_name}: Type mismatch"
elif isinstance(field_value, int):
assert prop_schema["type"] == "integer", f"{test_name}.{field_name}: Type mismatch"
elif isinstance(field_value, float):
assert prop_schema["type"] == "number", f"{test_name}.{field_name}: Type mismatch"
elif isinstance(field_value, list):
assert prop_schema["type"] == "array", f"{test_name}.{field_name}: Type mismatch"
elif isinstance(field_value, dict):
assert prop_schema["type"] == "object", f"{test_name}.{field_name}: Type mismatch"
@pytest.mark.skipif(not HAS_ROS2, reason="Missing dependencies")
def test_conversion_roundtrip_data(self):
"""测试转换往返的数据一致性"""
# 创建原始 ROS2 消息
string_msg = String()
string_msg.data = "roundtrip_test"
original_ros2_inst = ROS2MessageInstance(string_msg)
original_data = original_ros2_inst.get_python_dict()
# ROS2 -> JSONSchema
json_schema_inst = original_ros2_inst.to_json_schema()
converted_data = json_schema_inst.get_python_dict()
# 验证数据在转换过程中保持一致
assert original_data == converted_data
# 验证 Schema 的有效性
schema = json_schema_inst.json_schema
assert schema is not None
# 使用 jsonschema 库验证数据符合生成的 Schema
try:
jsonschema.validate(converted_data, schema)
except jsonschema.ValidationError as e:
pytest.fail(f"Generated data doesn't match generated schema: {e}")
@pytest.mark.skipif(not HAS_ROS2, reason="Missing dependencies")
def test_multiple_message_types_conversion(self):
"""测试多种消息类型的转换"""
test_messages = []
# String
string_msg = String()
string_msg.data = "multi_test_string"
test_messages.append(("String", string_msg))
# Float64MultiArray
array_msg = Float64MultiArray()
array_msg.data = [1.5, 2.5, 3.5]
test_messages.append(("Float64MultiArray", array_msg))
# Point
point_msg = Point()
point_msg.x = 1.0
point_msg.y = 2.0
point_msg.z = 3.0
test_messages.append(("Point", point_msg))
for msg_type_name, msg in test_messages:
# 创建 ROS2 实例
ros2_inst = ROS2MessageInstance(msg)
# 转换为 JSONSchema 实例
json_schema_inst = ros2_inst.to_json_schema()
# 验证转换成功
assert isinstance(json_schema_inst, JSONSchemaMessageInstance)
assert json_schema_inst.message_type == MessageType.JSON_SCHEMA
# 验证数据一致性
original_data = ros2_inst.get_python_dict()
converted_data = json_schema_inst.get_python_dict()
assert original_data == converted_data, f"{msg_type_name}: Data inconsistency after conversion"
# 验证 Schema 生成
schema = json_schema_inst.json_schema
assert schema is not None
assert schema["type"] == "object"
assert len(schema["properties"]) > 0, f"{msg_type_name}: No properties in generated schema"
class TestConversionErrorHandling:
"""转换错误处理测试"""
@pytest.mark.skipif(not HAS_ROS2, reason="Missing dependencies")
def test_empty_message_conversion(self):
"""测试空消息的转换"""
# 创建空的 String 消息
empty_string = String()
ros2_inst = ROS2MessageInstance(empty_string)
# 转换应该成功,即使数据为空
json_schema_inst = ros2_inst.to_json_schema()
assert isinstance(json_schema_inst, JSONSchemaMessageInstance)
# 验证 Schema 仍然有效
schema = json_schema_inst.json_schema
assert schema["type"] == "object"
assert "properties" in schema
@pytest.mark.skipif(not HAS_ROS2, reason="Missing dependencies")
def test_large_data_conversion(self):
"""测试大数据量的转换"""
# 创建包含大量数据的数组消息
large_array = Float64MultiArray()
large_array.data = [float(i) for i in range(1000)] # 1000 个浮点数
ros2_inst = ROS2MessageInstance(large_array)
# 转换应该能处理大数据量
json_schema_inst = ros2_inst.to_json_schema()
assert isinstance(json_schema_inst, JSONSchemaMessageInstance)
# 验证数据完整性
original_data = ros2_inst.get_python_dict()
converted_data = json_schema_inst.get_python_dict()
assert len(original_data["data"]) == 1000
assert len(converted_data["data"]) == 1000
assert original_data == converted_data
# 运行测试的便捷函数
def run_conversion_tests():
"""运行转换测试"""
if not HAS_ROS2:
print("❌ Required dependencies not available, skipping tests")
print(f" ROS2: {HAS_ROS2}")
return False
import subprocess
result = subprocess.run(
[
"python",
"-m",
"pytest",
"tests/test_ros_to_json_schema_conversion.py",
"-v",
"--tb=short",
],
capture_output=True,
text=True,
)
print(result.stdout)
if result.stderr:
print("STDERR:", result.stderr)
return result.returncode == 0
if __name__ == "__main__":
# 直接运行测试
run_conversion_tests()