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