429 lines
16 KiB
Python
429 lines
16 KiB
Python
|
#!/usr/bin/env python
|
|||
|
# -*- coding: utf-8 -*-
|
|||
|
|
|||
|
"""
|
|||
|
OPC UA模块测试
|
|||
|
测试VWED OPC UA内置函数模块的功能
|
|||
|
"""
|
|||
|
|
|||
|
import sys
|
|||
|
import os
|
|||
|
import time
|
|||
|
import threading
|
|||
|
import unittest
|
|||
|
|
|||
|
# 添加项目根目录到路径
|
|||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
|||
|
|
|||
|
from services.online_script.built_in_modules.opc_ua_module import VWEDOpcUaModule
|
|||
|
from tests.test_opc_ua_server import OpcUaTestServer
|
|||
|
|
|||
|
|
|||
|
class TestOpcUaModule(unittest.TestCase):
|
|||
|
"""OPC UA模块测试类"""
|
|||
|
|
|||
|
@classmethod
|
|||
|
def setUpClass(cls):
|
|||
|
"""设置测试类 - 启动OPC UA服务器"""
|
|||
|
print("\n" + "="*60)
|
|||
|
print("启动OPC UA测试服务器...")
|
|||
|
print("="*60)
|
|||
|
|
|||
|
# 启动测试服务器
|
|||
|
cls.server = OpcUaTestServer(host='localhost', port=4840)
|
|||
|
cls.server.start_server()
|
|||
|
|
|||
|
# 等待服务器完全启动
|
|||
|
time.sleep(2)
|
|||
|
|
|||
|
# 获取服务器信息
|
|||
|
cls.server_info = cls.server.get_server_info()
|
|||
|
cls.endpoint = cls.server_info['endpoint']
|
|||
|
cls.namespace_idx = cls.server_info['namespace_index']
|
|||
|
|
|||
|
print(f"服务器端点: {cls.endpoint}")
|
|||
|
print(f"命名空间索引: {cls.namespace_idx}")
|
|||
|
|
|||
|
# 显示测试节点信息
|
|||
|
node_info = cls.server.get_node_info()
|
|||
|
if node_info:
|
|||
|
print("\n可用测试节点:")
|
|||
|
for name, info in node_info.items():
|
|||
|
print(f" {name}: ns={info['namespace_index']};{info['node_id']} = {info['value']}")
|
|||
|
|
|||
|
@classmethod
|
|||
|
def tearDownClass(cls):
|
|||
|
"""清理测试类 - 停止OPC UA服务器"""
|
|||
|
print("\n" + "="*60)
|
|||
|
print("停止OPC UA测试服务器...")
|
|||
|
print("="*60)
|
|||
|
|
|||
|
if hasattr(cls, 'server'):
|
|||
|
cls.server.stop_server()
|
|||
|
time.sleep(1)
|
|||
|
|
|||
|
print("测试完成")
|
|||
|
|
|||
|
def setUp(self):
|
|||
|
"""设置测试方法"""
|
|||
|
self.opc_module = VWEDOpcUaModule("test_script")
|
|||
|
|
|||
|
def tearDown(self):
|
|||
|
"""清理测试方法"""
|
|||
|
if hasattr(self, 'opc_module'):
|
|||
|
self.opc_module.cleanup()
|
|||
|
|
|||
|
def test_read_opc_value_simple(self):
|
|||
|
"""测试简单读取OPC值(使用默认命名空间)"""
|
|||
|
print("\n测试: 简单读取OPC值")
|
|||
|
|
|||
|
# 注意:由于我们的测试节点都在自定义命名空间中,这个测试可能会失败
|
|||
|
# 这是预期的,因为默认命名空间(0)中通常没有我们的测试节点
|
|||
|
try:
|
|||
|
# 尝试读取标准的服务器状态节点
|
|||
|
value = self.opc_module.read_opc_value("i=2259", self.endpoint) # ServerStatus.State
|
|||
|
print(f"读取服务器状态: {value}")
|
|||
|
self.assertIsNotNone(value)
|
|||
|
except RuntimeError as e:
|
|||
|
print(f"预期的错误(默认命名空间中没有测试节点): {e}")
|
|||
|
self.assertEqual(str(e), "read_opc_value error")
|
|||
|
|
|||
|
def test_read_opc_value_with_namespace(self):
|
|||
|
"""测试带命名空间的OPC值读取"""
|
|||
|
print("\n测试: 带命名空间的OPC值读取")
|
|||
|
|
|||
|
# 测试读取整数节点
|
|||
|
try:
|
|||
|
value = self.opc_module.read_opc_value_with_namespace(
|
|||
|
self.namespace_idx, "i=1001", self.endpoint
|
|||
|
)
|
|||
|
print(f"读取整数节点: ns={self.namespace_idx};i=1001 = {value}")
|
|||
|
self.assertIsNotNone(value)
|
|||
|
self.assertEqual(value, 1001)
|
|||
|
except Exception as e:
|
|||
|
self.fail(f"读取整数节点失败: {e}")
|
|||
|
|
|||
|
# 测试读取字符串节点
|
|||
|
try:
|
|||
|
value = self.opc_module.read_opc_value_with_namespace(
|
|||
|
self.namespace_idx, "s=StringNodeId", self.endpoint
|
|||
|
)
|
|||
|
print(f"读取字符串节点: ns={self.namespace_idx};s=StringNodeId = {value}")
|
|||
|
self.assertIsNotNone(value)
|
|||
|
self.assertEqual(value, "String Node Value")
|
|||
|
except Exception as e:
|
|||
|
self.fail(f"读取字符串节点失败: {e}")
|
|||
|
|
|||
|
# 测试读取GUID节点
|
|||
|
try:
|
|||
|
value = self.opc_module.read_opc_value_with_namespace(
|
|||
|
self.namespace_idx, "g=550e8400-e29b-41d4-a716-446655440000", self.endpoint
|
|||
|
)
|
|||
|
print(f"读取GUID节点: ns={self.namespace_idx};g=550e8400-e29b-41d4-a716-446655440000 = {value}")
|
|||
|
self.assertIsNotNone(value)
|
|||
|
self.assertEqual(value, "GUID Node Value")
|
|||
|
except Exception as e:
|
|||
|
self.fail(f"读取GUID节点失败: {e}")
|
|||
|
|
|||
|
def test_read_dynamic_values(self):
|
|||
|
"""测试读取动态变化的值"""
|
|||
|
print("\n测试: 读取动态变化的值")
|
|||
|
|
|||
|
# 读取计数器值
|
|||
|
try:
|
|||
|
value1 = self.opc_module.read_opc_value_with_namespace(
|
|||
|
self.namespace_idx, "Counter", self.endpoint
|
|||
|
)
|
|||
|
print(f"第一次读取计数器: {value1}")
|
|||
|
|
|||
|
# 等待一段时间
|
|||
|
time.sleep(2)
|
|||
|
|
|||
|
value2 = self.opc_module.read_opc_value_with_namespace(
|
|||
|
self.namespace_idx, "Counter", self.endpoint
|
|||
|
)
|
|||
|
print(f"第二次读取计数器: {value2}")
|
|||
|
|
|||
|
# 验证值发生了变化
|
|||
|
self.assertNotEqual(value1, value2)
|
|||
|
|
|||
|
except Exception as e:
|
|||
|
self.fail(f"读取动态值失败: {e}")
|
|||
|
|
|||
|
def test_write_opc_value(self):
|
|||
|
"""测试写入OPC值"""
|
|||
|
print("\n测试: 写入OPC值")
|
|||
|
|
|||
|
# 测试写入整数值
|
|||
|
try:
|
|||
|
# 先读取当前值
|
|||
|
original_value = self.opc_module.read_opc_value_with_namespace(
|
|||
|
self.namespace_idx, "i=1001", self.endpoint
|
|||
|
)
|
|||
|
print(f"原始值: {original_value}")
|
|||
|
|
|||
|
# 写入新值
|
|||
|
new_value = 9999
|
|||
|
result = self.opc_module.write_opc_value_with_namespace(
|
|||
|
self.namespace_idx, "i=1001", new_value, self.endpoint
|
|||
|
)
|
|||
|
print(f"写入结果: {result}")
|
|||
|
self.assertTrue(result)
|
|||
|
|
|||
|
# 读取验证
|
|||
|
written_value = self.opc_module.read_opc_value_with_namespace(
|
|||
|
self.namespace_idx, "i=1001", self.endpoint
|
|||
|
)
|
|||
|
print(f"写入后的值: {written_value}")
|
|||
|
self.assertEqual(written_value, new_value)
|
|||
|
|
|||
|
# 恢复原始值
|
|||
|
self.opc_module.write_opc_value_with_namespace(
|
|||
|
self.namespace_idx, "i=1001", original_value, self.endpoint
|
|||
|
)
|
|||
|
|
|||
|
except Exception as e:
|
|||
|
self.fail(f"写入值失败: {e}")
|
|||
|
|
|||
|
def test_write_different_types(self):
|
|||
|
"""测试写入不同类型的值"""
|
|||
|
print("\n测试: 写入不同类型的值")
|
|||
|
|
|||
|
test_cases = [
|
|||
|
("BooleanVariable", True, False),
|
|||
|
("IntegerVariable", 42, 100),
|
|||
|
("FloatVariable", 3.14159, 2.71828),
|
|||
|
("StringVariable", "Hello VWED", "Hello Test"),
|
|||
|
]
|
|||
|
|
|||
|
for node_name, original_expected, new_value in test_cases:
|
|||
|
try:
|
|||
|
print(f"\n测试节点: {node_name}")
|
|||
|
|
|||
|
# 读取当前值
|
|||
|
current_value = self.opc_module.read_opc_value_with_namespace(
|
|||
|
self.namespace_idx, node_name, self.endpoint
|
|||
|
)
|
|||
|
print(f" 当前值: {current_value} ({type(current_value).__name__})")
|
|||
|
|
|||
|
# 写入新值
|
|||
|
result = self.opc_module.write_opc_value_with_namespace(
|
|||
|
self.namespace_idx, node_name, new_value, self.endpoint
|
|||
|
)
|
|||
|
print(f" 写入 {new_value} 结果: {result}")
|
|||
|
self.assertTrue(result)
|
|||
|
|
|||
|
# 验证写入
|
|||
|
written_value = self.opc_module.read_opc_value_with_namespace(
|
|||
|
self.namespace_idx, node_name, self.endpoint
|
|||
|
)
|
|||
|
print(f" 写入后的值: {written_value} ({type(written_value).__name__})")
|
|||
|
self.assertEqual(written_value, new_value)
|
|||
|
|
|||
|
# 恢复原始值(如果知道的话)
|
|||
|
if current_value is not None:
|
|||
|
self.opc_module.write_opc_value_with_namespace(
|
|||
|
self.namespace_idx, node_name, current_value, self.endpoint
|
|||
|
)
|
|||
|
|
|||
|
except Exception as e:
|
|||
|
print(f" 测试 {node_name} 失败: {e}")
|
|||
|
|
|||
|
def test_subscription_read(self):
|
|||
|
"""测试订阅方式读取值"""
|
|||
|
print("\n测试: 订阅方式读取值")
|
|||
|
|
|||
|
try:
|
|||
|
# 第一次读取(创建订阅)
|
|||
|
value1 = self.opc_module.read_opc_value_by_subscription(
|
|||
|
self.namespace_idx, "Counter", self.endpoint
|
|||
|
)
|
|||
|
print(f"订阅读取计数器(第一次): {value1}")
|
|||
|
self.assertIsNotNone(value1)
|
|||
|
|
|||
|
# 等待一段时间让值发生变化
|
|||
|
time.sleep(3)
|
|||
|
|
|||
|
# 第二次读取(使用已有订阅)
|
|||
|
value2 = self.opc_module.read_opc_value_by_subscription(
|
|||
|
self.namespace_idx, "Counter", self.endpoint
|
|||
|
)
|
|||
|
print(f"订阅读取计数器(第二次): {value2}")
|
|||
|
self.assertIsNotNone(value2)
|
|||
|
|
|||
|
# 验证值发生了变化(或至少订阅在工作)
|
|||
|
print(f"值变化: {value1} -> {value2}")
|
|||
|
|
|||
|
except Exception as e:
|
|||
|
self.fail(f"订阅读取失败: {e}")
|
|||
|
|
|||
|
def test_error_handling(self):
|
|||
|
"""测试错误处理"""
|
|||
|
print("\n测试: 错误处理")
|
|||
|
|
|||
|
# 测试连接到不存在的服务器
|
|||
|
try:
|
|||
|
self.opc_module.read_opc_value_with_namespace(
|
|||
|
0, "i=1", "opc.tcp://nonexistent:4840"
|
|||
|
)
|
|||
|
self.fail("应该抛出RuntimeError异常")
|
|||
|
except RuntimeError as e:
|
|||
|
print(f"预期的连接错误: {e}")
|
|||
|
self.assertEqual(str(e), "read_opc_value error")
|
|||
|
|
|||
|
# 测试读取不存在的节点
|
|||
|
try:
|
|||
|
self.opc_module.read_opc_value_with_namespace(
|
|||
|
self.namespace_idx, "i=99999", self.endpoint
|
|||
|
)
|
|||
|
self.fail("应该抛出RuntimeError异常")
|
|||
|
except RuntimeError as e:
|
|||
|
print(f"预期的节点不存在错误: {e}")
|
|||
|
self.assertEqual(str(e), "read_opc_value error")
|
|||
|
|
|||
|
def test_cleanup(self):
|
|||
|
"""测试资源清理"""
|
|||
|
print("\n测试: 资源清理")
|
|||
|
|
|||
|
# 创建一些连接和订阅
|
|||
|
try:
|
|||
|
self.opc_module.read_opc_value_with_namespace(
|
|||
|
self.namespace_idx, "i=1001", self.endpoint
|
|||
|
)
|
|||
|
self.opc_module.read_opc_value_by_subscription(
|
|||
|
self.namespace_idx, "Counter", self.endpoint
|
|||
|
)
|
|||
|
|
|||
|
# 验证有连接和订阅
|
|||
|
self.assertGreater(len(self.opc_module._clients), 0)
|
|||
|
self.assertGreater(len(self.opc_module._subscriptions), 0)
|
|||
|
|
|||
|
# 清理资源
|
|||
|
self.opc_module.cleanup()
|
|||
|
|
|||
|
# 验证资源已清理
|
|||
|
self.assertEqual(len(self.opc_module._clients), 0)
|
|||
|
self.assertEqual(len(self.opc_module._subscriptions), 0)
|
|||
|
self.assertEqual(len(self.opc_module._subscription_values), 0)
|
|||
|
|
|||
|
print("资源清理成功")
|
|||
|
|
|||
|
except Exception as e:
|
|||
|
self.fail(f"资源清理失败: {e}")
|
|||
|
|
|||
|
|
|||
|
def run_interactive_test():
|
|||
|
"""运行交互式测试"""
|
|||
|
print("\n" + "="*60)
|
|||
|
print("OPC UA模块交互式测试")
|
|||
|
print("="*60)
|
|||
|
|
|||
|
# 启动服务器
|
|||
|
server = OpcUaTestServer(host='localhost', port=4840)
|
|||
|
server.start_server()
|
|||
|
time.sleep(2)
|
|||
|
|
|||
|
# 创建模块实例
|
|||
|
opc_module = VWEDOpcUaModule("interactive_test")
|
|||
|
|
|||
|
server_info = server.get_server_info()
|
|||
|
endpoint = server_info['endpoint']
|
|||
|
namespace_idx = server_info['namespace_index']
|
|||
|
|
|||
|
print(f"服务器端点: {endpoint}")
|
|||
|
print(f"命名空间索引: {namespace_idx}")
|
|||
|
|
|||
|
try:
|
|||
|
# 显示可用节点
|
|||
|
node_info = server.get_node_info()
|
|||
|
print(f"\n可用测试节点:")
|
|||
|
for name, info in node_info.items():
|
|||
|
print(f" {name}: ns={info['namespace_index']};{info['node_id']} = {info['value']}")
|
|||
|
|
|||
|
print(f"\n开始交互式测试...")
|
|||
|
|
|||
|
# 测试各种操作
|
|||
|
print(f"\n1. 读取各种类型的节点:")
|
|||
|
test_reads = [
|
|||
|
("i=1001", "整数节点"),
|
|||
|
("s=StringNodeId", "字符串节点"),
|
|||
|
("BooleanVariable", "布尔变量"),
|
|||
|
("Temperature", "温度传感器"),
|
|||
|
("Counter", "计数器"),
|
|||
|
]
|
|||
|
|
|||
|
for node_id, description in test_reads:
|
|||
|
try:
|
|||
|
value = opc_module.read_opc_value_with_namespace(namespace_idx, node_id, endpoint)
|
|||
|
print(f" {description} ({node_id}): {value}")
|
|||
|
except Exception as e:
|
|||
|
print(f" {description} ({node_id}): 读取失败 - {e}")
|
|||
|
|
|||
|
print(f"\n2. 测试写入操作:")
|
|||
|
# 写入整数值
|
|||
|
try:
|
|||
|
result = opc_module.write_opc_value_with_namespace(namespace_idx, "i=1001", 8888, endpoint)
|
|||
|
print(f" 写入整数节点 8888: {result}")
|
|||
|
|
|||
|
# 验证写入
|
|||
|
value = opc_module.read_opc_value_with_namespace(namespace_idx, "i=1001", endpoint)
|
|||
|
print(f" 读取验证: {value}")
|
|||
|
except Exception as e:
|
|||
|
print(f" 写入失败: {e}")
|
|||
|
|
|||
|
print(f"\n3. 测试订阅读取:")
|
|||
|
try:
|
|||
|
value1 = opc_module.read_opc_value_by_subscription(namespace_idx, "Counter", endpoint)
|
|||
|
print(f" 订阅计数器(第一次): {value1}")
|
|||
|
|
|||
|
time.sleep(3)
|
|||
|
|
|||
|
value2 = opc_module.read_opc_value_by_subscription(namespace_idx, "Counter", endpoint)
|
|||
|
print(f" 订阅计数器(第二次): {value2}")
|
|||
|
print(f" 值变化: {value1} -> {value2}")
|
|||
|
except Exception as e:
|
|||
|
print(f" 订阅失败: {e}")
|
|||
|
|
|||
|
print(f"\n4. 监控动态值变化 (10秒):")
|
|||
|
start_time = time.time()
|
|||
|
while time.time() - start_time < 10:
|
|||
|
try:
|
|||
|
counter = opc_module.read_opc_value_with_namespace(namespace_idx, "Counter", endpoint)
|
|||
|
temp = opc_module.read_opc_value_with_namespace(namespace_idx, "Temperature", endpoint)
|
|||
|
print(f" [{time.strftime('%H:%M:%S')}] 计数器: {counter}, 温度: {temp:.2f}")
|
|||
|
time.sleep(2)
|
|||
|
except Exception as e:
|
|||
|
print(f" 监控错误: {e}")
|
|||
|
break
|
|||
|
|
|||
|
except KeyboardInterrupt:
|
|||
|
print("\n用户中断测试")
|
|||
|
finally:
|
|||
|
print(f"\n清理资源...")
|
|||
|
opc_module.cleanup()
|
|||
|
server.stop_server()
|
|||
|
print("交互式测试完成")
|
|||
|
|
|||
|
|
|||
|
def main():
|
|||
|
"""主函数"""
|
|||
|
print("VWED OPC UA模块测试")
|
|||
|
print("请选择测试模式:")
|
|||
|
print("1. 运行单元测试")
|
|||
|
print("2. 运行交互式测试")
|
|||
|
|
|||
|
choice = input("请输入选择 (1 或 2): ").strip()
|
|||
|
|
|||
|
if choice == "1":
|
|||
|
# 运行单元测试
|
|||
|
unittest.main(argv=[''], exit=False, verbosity=2)
|
|||
|
elif choice == "2":
|
|||
|
# 运行交互式测试
|
|||
|
run_interactive_test()
|
|||
|
else:
|
|||
|
print("无效选择,运行单元测试")
|
|||
|
unittest.main(argv=[''], exit=False, verbosity=2)
|
|||
|
|
|||
|
|
|||
|
if __name__ == "__main__":
|
|||
|
main()
|