VWED_server/tests/test_opc_ua_module.py

429 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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()