first commit

This commit is contained in:
2026-06-16 15:40:19 +08:00
commit 8ba6324244
97 changed files with 6216 additions and 0 deletions

22
tests/test_udp_connect.py Normal file
View File

@@ -0,0 +1,22 @@
import socket
import psutil
from midware.config.base_config import DEVICE_CONFIG_DICT, UDP_CONFIG
def check_port_listen(ip, port):
"""检测指定IP:端口是否被监听"""
for conn in psutil.net_connections(kind='inet'):
if conn.laddr.ip == ip and conn.laddr.port == port and conn.status == 'LISTEN':
return True
return False
if __name__ == "__main__":
local_ip = UDP_CONFIG["LOCAL_IP"]
# 检测所有外设UDP端口
print("===== UDP端口监听状态检测 =====")
for dev_name, dev_info in DEVICE_CONFIG_DICT.items():
port = dev_info["udp_port"]
is_listen = check_port_listen(local_ip, port)
status = "✅ 正常监听" if is_listen else "❌ 未监听"
print(f"{dev_name} | {local_ip}:{port} | {status}")
# 检测故障注入软件对接端口(非监听,为发送套接字)
print(f"\n故障注入软件接收端口:{local_ip}:{UDP_CONFIG['FAULT_INJECT_PORT']}(自测客户端监听)")

295
tests/test_udp_connect_2.py Normal file
View File

@@ -0,0 +1,295 @@
# 注释掉/删除Linux的shebang行替换为Windows兼容的写法可选
# #!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
故障注入平台UDP模拟客户端
功能向中间件多UDP端口发送16进制流数据模拟故障注入平台的发包行为
作者:工程测试专用
日期2026-02-26
"""
"""
该程序专门模拟故障注入平台的 UDP 数据发送行为,向中间件的多 UDP 端口发送符合协议格式的 16 进制流数据可验证中间件udp_handler.py的多端口非阻塞接收、数据解析、端口隔离等核心功能支持单端口定向发送、多端口并发发送、自定义数据格式全程适配工程的 16 进制流数据规范。
一、测试程序核心功能
模拟故障注入平台向中间件指定 UDP 端口发送 16 进制流数据;
支持单端口循环发送、多端口并发发送(验证端口隔离性);
可自定义测试数据(原始二进制 / 16 进制字符串),自动适配工程数据格式;
实时打印发送日志,包含目标端口、数据长度、发送状态;
支持按协议类型UART/CAN/1553B/AD/OC批量发送对应外设数据。
"""
"""
使用步骤
步骤 1配置适配
修改DEVICE_UDP_PORTS字典确保与中间件device_config.csv中的外设名称 + UDP 端口完全一致;
确认MIDDLEWARE_IP为中间件运行的 IP本地测试填127.0.0.1,跨机器测试填中间件所在机器 IP
调整TEST_CONFIG中的参数
send_interval发送间隔默认 1ms模拟实时数据
concurrent_threads并发线程数默认 5可根据机器性能调整
send_times_per_port每个端口发送次数默认 1000 次。
步骤 2运行准备
先启动中间件主程序main.py确保
配置初始化成功(日志显示 “加载 X 个外设”);
UDP 服务初始化完成(日志显示 “监听端口:[8880,8881,...]”);
安装依赖(仅需 loguru
bash
运行
pip install loguru
步骤 3执行测试
将测试程序保存为fault_inject_simulator.py放在工程根目录
运行测试程序:
bash
运行
python fault_inject_simulator.py
可选测试模式:
单端口测试取消send_single_port_cycle注释测试指定外设端口的接收能力
多端口并发测试取消send_multi_port_concurrent注释测试多端口隔离性和并发接收能力。
"""
import sys
import os
import io
# 设置标准输出和标准错误的编码为UTF-8
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
import socket
import binascii
import time
import threading
import random
from loguru import logger
from pathlib import Path
# 获取当前文件的目录
current_dir = os.path.dirname(os.path.abspath(__file__))
# 获取上级目录(项目根目录)
parent_dir = os.path.dirname(current_dir)
# 将项目根目录添加到系统路径
sys.path.append(parent_dir)
#sys.path.append(str(Path(__file__).parent.parent.parent))
from midware.config.base_config import UDP_CONFIG
# ==================== 配置项与中间件base_config.py保持一致====================
# 中间件本地IP故障注入平台访问的IP
MIDDLEWARE_IP = "127.0.0.1"
# 故障注入平台接收端口(中间件回传数据的端口,可选)
FAULT_RECV_PORT = 8889
# 中间件监听的外设UDP端口列表从device_config.csv复制
DEVICE_UDP_PORTS = {
"Uart0": 4000,
"CAN0": 4001,#": 8881,
# "1553B0": 8882,
# "AD0": 8883,
# "OC0": 8884,
# "Uart1": 8885,
# "CAN2": 8886,
# "1553B1": 8887,
# "AD1": 8888,
# "OC1": 8889
}
# 测试数据配置
TEST_CONFIG = {
"buffer_size": 32, # UDP缓冲区大小,4096
"send_interval": 0.1, # 单端口发送间隔1ms模拟实时数据
"concurrent_threads": 5, # 并发发送线程数
"send_times_per_port": 10 # 每个端口循环发送次数
}
# ==================== 日志配置 ====================
logger.add("fault_inject_simulator.log", level="INFO",
format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {message}",
rotation="10MB", retention="3 days")
class FaultInjectUDPSimulator:
"""故障注入平台UDP模拟客户端"""
def __init__(self):
# 创建非阻塞UDP发送套接字
self.send_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.send_sock.setblocking(False)
# 开启地址复用,避免端口占用
self.send_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 发送统计
self.send_stats = {
"total_send": 0,
"success_send": 0,
"failed_send": 0,
"port_stats": {} # 按端口统计:{port: {"success":0, "failed":0}}
}
# 初始化端口统计
for port in DEVICE_UDP_PORTS.values():
self.send_stats["port_stats"][port] = {"success": 0, "failed": 0}
logger.info("故障注入平台UDP模拟器初始化完成")
def _raw_to_hex_str(self, raw_data: bytes) -> str:
"""
原始二进制数据转16进制字符串符合中间件数据格式要求
:param raw_data: 原始二进制数据
:return: 16进制字符串如b"test""74657374"
"""
return binascii.hexlify(raw_data).decode("utf-8")
def send_data_to_port(self, port: int, raw_data: bytes, dev_name: str = "unknown"):
"""
向指定UDP端口发送数据核心方法
:param port: 中间件监听的UDP端口
:param raw_data: 原始二进制数据自动转为16进制流
:param dev_name: 外设名称(用于日志)
"""
try:
# 转换为16进制字符串中间件要求的格式
hex_data = self._raw_to_hex_str(raw_data)
# 非阻塞发送数据
self.send_sock.sendto(hex_data.encode("utf-8"), (MIDDLEWARE_IP, port))
# 更新统计
self.send_stats["total_send"] += 1
self.send_stats["success_send"] += 1
self.send_stats["port_stats"][port]["success"] += 1
logger.debug(
f"发送成功 | 外设:{dev_name} | 端口:{port} | "
f"原始数据长度:{len(raw_data)}字节 | 16进制流{hex_data[:32]}..."
)
return True
except BlockingIOError:
# 非阻塞发送缓冲区满
self.send_stats["total_send"] += 1
self.send_stats["failed_send"] += 1
self.send_stats["port_stats"][port]["failed"] += 1
logger.warning(f"发送失败 | 外设:{dev_name} | 端口:{port} | 原因:发送缓冲区满")
return False
except Exception as e:
self.send_stats["total_send"] += 1
self.send_stats["failed_send"] += 1
self.send_stats["port_stats"][port]["failed"] += 1
logger.error(
f"发送异常 | 外设:{dev_name} | 端口:{port} | 错误:{str(e)}",
exc_info=True
)
return False
def send_single_port_cycle(self, dev_name: str, send_times: int = None):
"""
单端口循环发送测试数据
:param dev_name: 外设名称(如"Uart0"对应DEVICE_UDP_PORTS中的key
:param send_times: 发送次数默认使用TEST_CONFIG中的配置
"""
if dev_name not in DEVICE_UDP_PORTS:
logger.error(f"外设{dev_name}不存在,可选外设:{list(DEVICE_UDP_PORTS.keys())}")
return
port = DEVICE_UDP_PORTS[dev_name]
send_times = send_times or TEST_CONFIG["send_times_per_port"]
logger.info(f"开始单端口循环发送 | 外设:{dev_name} | 端口:{port} | 发送次数:{send_times}")
# 按协议类型生成测试数据(模拟真实外设数据)
if "UART" in dev_name:
raw_data = b"UART_DATA_" + str(random.randint(1000, 9999)).encode() + b"\x9c\x47" # 带CRC16
elif "CAN" in dev_name:
raw_data = b"CAN_DATA_" + str(random.randint(1000, 9999)).encode() + b"\x1a\x2b" # 带CRC16
elif "1553B" in dev_name:
raw_data = b"1553B_DATA_" + str(random.randint(1000, 9999)).encode() + b"\x0f\x3d" # 带CRC16
elif "AD" in dev_name:
raw_data = b"AD_DATA_" + str(random.randint(1000, 9999)).encode() + b"\x5f" # 带累加和
elif "OC" in dev_name:
raw_data = b"OC_DATA_" + str(random.randint(1000, 9999)).encode() + b"\x6a" # 带累加和
else:
raw_data = b"TEST_DATA_" + str(random.randint(1000, 9999)).encode()
# 循环发送
for i in range(send_times):
self.send_data_to_port(port, raw_data, dev_name)
time.sleep(TEST_CONFIG["send_interval"])
logger.info(f"单端口发送完成 | 外设:{dev_name} | 端口:{port} | 发送统计:{self.send_stats['port_stats'][port]}")
def send_multi_port_concurrent(self, dev_names: list = None, send_times: int = None):
"""
多端口并发发送测试(验证端口隔离性)
:param dev_names: 外设名称列表,默认发送所有外设
:param send_times: 每个端口发送次数默认使用TEST_CONFIG中的配置
"""
dev_names = dev_names or list(DEVICE_UDP_PORTS.keys())
send_times = send_times or TEST_CONFIG["send_times_per_port"]
logger.info(f"开始多端口并发发送 | 外设列表:{dev_names} | 每端口发送次数:{send_times}")
# 定义单个端口的发送任务
def send_task(dev_name):
port = DEVICE_UDP_PORTS[dev_name]
# 生成对应协议的测试数据
if "UART" in dev_name:
raw_data = b"UART_CONCURRENT_" + dev_name.encode() + b"_" + str(random.randint(1000, 9999)).encode()
elif "CAN" in dev_name:
raw_data = b"CAN_CONCURRENT_" + dev_name.encode() + b"_" + str(random.randint(1000, 9999)).encode()
elif "1553B" in dev_name:
raw_data = b"1553B_CONCURRENT_" + dev_name.encode() + b"_" + str(random.randint(1000, 9999)).encode()
else:
raw_data = b"CONCURRENT_" + dev_name.encode() + b"_" + str(random.randint(1000, 9999)).encode()
# 循环发送
for i in range(send_times):
self.send_data_to_port(port, raw_data, dev_name)
time.sleep(TEST_CONFIG["send_interval"])
# 创建并发线程
threads = []
max_threads = TEST_CONFIG["concurrent_threads"]
active_threads = 0
for dev_name in dev_names:
if dev_name not in DEVICE_UDP_PORTS:
logger.warning(f"外设{dev_name}不存在,跳过")
continue
# 控制并发线程数,避免资源耗尽
while active_threads >= max_threads:
time.sleep(0.001)
active_threads = sum(1 for t in threads if t.is_alive())
# 启动线程
t = threading.Thread(target=send_task, args=(dev_name,), daemon=True)
threads.append(t)
t.start()
active_threads += 1
logger.debug(f"启动并发发送线程 | 外设:{dev_name} | 线程ID{t.ident}")
# 等待所有线程完成
for t in threads:
t.join()
logger.info("多端口并发发送完成 | 全局统计:"
f"总发送{self.send_stats['total_send']} | "
f"成功{self.send_stats['success_send']} | "
f"失败{self.send_stats['failed_send']}")
def print_send_stats(self):
"""打印最终发送统计"""
logger.info("="*50 + " 发送统计汇总 " + "="*50)
logger.info(f"全局统计 | 总发送:{self.send_stats['total_send']} | 成功:{self.send_stats['success_send']} | 失败:{self.send_stats['failed_send']}")
logger.info("端口统计:")
for port, stats in self.send_stats["port_stats"].items():
dev_name = [k for k, v in DEVICE_UDP_PORTS.items() if v == port][0]
total = stats["success"] + stats["failed"]
success_rate = (stats["success"] / total * 100) if total > 0 else 0
logger.info(f" 外设:{dev_name:6s} | 端口:{port:5d} | 发送:{total:5d} | 成功:{stats['success']:5d} | 失败:{stats['failed']:5d} | 成功率:{success_rate:.2f}%")
logger.info("="*100)
def close(self):
"""关闭套接字,释放资源"""
self.send_sock.close()
logger.info("故障注入平台UDP模拟器已关闭资源释放完成")
# ==================== 测试执行入口 ====================
def main():
# 初始化模拟器
simulator = FaultInjectUDPSimulator()
print("故障注入平台UDP模拟器已启动按Ctrl+C终止测试")
try:
# 可选测试模式:二选一或全选
# 模式1单端口循环发送测试Uart0
# simulator.send_single_port_cycle("Uart0", send_times=50)
# 模式2多端口并发发送测试所有外设
simulator.send_multi_port_concurrent(dev_names=["Uart0", "CAN0", "1553B0", "AD0", "OC0"], send_times=20)
# 打印统计结果
simulator.print_send_stats()
except KeyboardInterrupt:
logger.info("用户终止测试,打印最终统计...")
simulator.print_send_stats()
finally:
# 关闭模拟器
simulator.close()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,148 @@
"""
验证工程满足1553B 6-12us 响应、同时支持 10 个单机接入、CAN/1553B 消息顺序发送的性能需求在test/下创建test_udp_performance.py
2026/2/26
Tan Mingyan
"""
import socket
import binascii
import time
import threading
from midware.config.base_config import DEVICE_CONFIG_DICT, UDP_CONFIG
# 全局配置
LOCAL_IP = UDP_CONFIG["LOCAL_IP"]
FAULT_PORT = UDP_CONFIG["FAULT_INJECT_PORT"]
BUFFER_SIZE = 4096
# 测试用1553B外设端口取CSV中的1553B0/1553B1如8882/8886
B1553B_PORTS = [dev_info["udp_port"] for dev_name, dev_info in DEVICE_CONFIG_DICT.items() if dev_info["protocol"] == "1553B"]
# CAN外设端口
CAN_PORTS = [dev_info["udp_port"] for dev_name, dev_info in DEVICE_CONFIG_DICT.items() if dev_info["protocol"] == "CAN"]
# 10个测试端口覆盖所有协议
TEST_PORTS = [dev_info["udp_port"] for dev_name, dev_info in DEVICE_CONFIG_DICT.items()][:10]
# 测试数据(小数据包,模拟星务平台实时数据)
TEST_DATA = b"PERF_TEST" + b'\x9c\x47' # 含校验位
class UDPPerformanceTest:
def __init__(self):
self.send_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.send_sock.setblocking(False)
# 接收套接字(统计回传时延)
self.recv_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.recv_sock.bind((LOCAL_IP, FAULT_PORT))
self.recv_sock.setblocking(False)
# 时延统计
self.latency_list = []
# 并发计数
self.send_count = 0
self.recv_count = 0
def _calc_latency(self, port, send_ts):
"""计算单包时延:发送时间→接收回传时间"""
try:
data, _ = self.recv_sock.recvfrom(BUFFER_SIZE)
recv_ts = time.time()
latency = (recv_ts - send_ts) * 1000000 # 转换为微秒(us)
self.latency_list.append(latency)
self.recv_count += 1
print(f"1553B端口{port} | 响应时延:{latency:.2f}us | 累计接收:{self.recv_count}")
except:
pass
def test_1553b_latency(self, test_times=1000):
"""测试1553B响应时延要求6-12us发送test_times次数据包"""
if not B1553B_PORTS:
print("❌ 未配置1553B外设跳过时延测试")
return
test_port = B1553B_PORTS[0]
print(f"===== 1553B时延测试端口{test_port},发送{test_times}次) =====")
self.latency_list.clear()
self.recv_count = 0
for i in range(test_times):
send_ts = time.time()
# 发送数据
hex_data = binascii.hexlify(TEST_DATA).decode("utf-8")
self.send_sock.sendto(hex_data.encode("utf-8"), (LOCAL_IP, test_port))
# 立即统计时延(模拟中间层实时回传)
self._calc_latency(test_port, send_ts)
# 微秒级间隔,模拟星务平台请求频率
time.sleep(0.000001)
# 统计结果
if self.latency_list:
avg_latency = sum(self.latency_list) / len(self.latency_list)
min_latency = min(self.latency_list)
max_latency = max(self.latency_list)
print(f"\n1553B时延统计 | 平均:{avg_latency:.2f}us | 最小:{min_latency:.2f}us | 最大:{max_latency:.2f}us")
if avg_latency >=6 and avg_latency <=12:
print("✅ 1553B时延满足要求6-12us")
else:
print("❌ 1553B时延不满足要求")
def test_10_device_concurrent(self, test_times=500):
"""测试10个单机并发接入同时发送验证无丢包"""
if len(TEST_PORTS) <10:
print("❌ 未配置10个外设跳过多机并发测试")
return
print(f"===== 10机并发测试发送{test_times}次/机) =====")
self.send_count = 0
self.recv_count = 0
def send_task(port):
for _ in range(test_times):
hex_data = binascii.hexlify(TEST_DATA).decode("utf-8")
self.send_sock.sendto(hex_data.encode("utf-8"), (LOCAL_IP, port))
self.send_count +=1
time.sleep(0.0001)
# 启动10个线程对应10个外设
threads = [threading.Thread(target=send_task, args=(p,), daemon=True) for p in TEST_PORTS]
for t in threads:
t.start()
for t in threads:
t.join()
# 等待接收完成
time.sleep(2)
loss_rate = (self.send_count - self.recv_count) / self.send_count * 100
print(f"总发送:{self.send_count} | 总接收:{self.recv_count} | 丢包率:{loss_rate:.2f}%")
if loss_rate == 0:
print("✅ 10机并发测试通过无丢包")
else:
print("❌ 10机并发测试失败存在丢包")
def test_can_b1553b_order(self):
"""测试CAN/1553B消息顺序发送要求一个节点发完再发下一个"""
print("===== CAN/1553B消息顺序测试 =====")
# 按顺序发送多个节点数据,查看中间层日志
test_ports = CAN_PORTS[:2] + B1553B_PORTS[:2]
for port in test_ports:
hex_data = binascii.hexlify(TEST_DATA + str(port).encode()).decode("utf-8")
self.send_sock.sendto(hex_data.encode("utf-8"), (LOCAL_IP, port))
print(f"按顺序发送:端口{port} 数据标识:{port}")
time.sleep(0.001)
print("✅ 请查看工程logs/run_*.log验证日志中节点数据是否按发送顺序打印无乱序")
def close(self):
self.send_sock.close()
self.recv_sock.close()
if __name__ == "__main__":
perf_test = UDPPerformanceTest()
try:
# 1. 1553B时延测试发送1000次
perf_test.test_1553b_latency(test_times=1000)
time.sleep(3)
# 2. 10机并发测试每机发送500次
perf_test.test_10_device_concurrent(test_times=500)
time.sleep(3)
# 3. CAN/1553B消息顺序测试
perf_test.test_can_b1553b_order()
except KeyboardInterrupt:
print("\n用户终止性能测试")
finally:
perf_test.close()

410
tests/udp_auto_test.py Normal file
View File

@@ -0,0 +1,410 @@
"""
虚拟仿真平台中间层软件 - UDP 一键自测脚本
该脚本整合连接性检测、单 / 多端口传输测试、1553B 时延检测、10 机并发检测、CAN/1553B 消息顺序检测所有自测项一键运行自动执行并生成可视化自测报告无需手动分步操作报告保存至test/report/目录,同时输出控制台日志,适配工程现有架构。
"""
import socket
import binascii
import time
import threading
import psutil
import os
from datetime import datetime
from prettytable import PrettyTable
from loguru import logger
# 工程配置导入需确保工程根目录在Python环境变量
try:
from midware.config.base_config import DEVICE_CONFIG_DICT, UDP_CONFIG
from midware.network.udp_handler import udp_server
except ImportError as e:
print(f"❌ 导入工程模块失败请将工程根目录加入Python环境变量{e}")
exit(1)
# 全局自测配置
LOCAL_IP = UDP_CONFIG["LOCAL_IP"]
FAULT_INJECT_IP = UDP_CONFIG["FAULT_INJECT_IP"]
FAULT_INJECT_PORT = UDP_CONFIG["FAULT_INJECT_PORT"]
BUFFER_SIZE = UDP_CONFIG["BUFFER_SIZE"]
TEST_TIMES_1553B = 1000 # 1553B时延测试发送次数
TEST_TIMES_CONCURRENT = 500 # 10机并发每机发送次数
TEST_DATA = b"UDP_AUTO_TEST" + b'\x9c\x47' # 带CRC16校验位的测试数据
# 协议端口筛选
B1553B_PORTS = [dev_info["udp_port"] for dev_name, dev_info in DEVICE_CONFIG_DICT.items() if dev_info["protocol"] == "1553B"]
CAN_PORTS = [dev_info["udp_port"] for dev_name, dev_info in DEVICE_CONFIG_DICT.items() if dev_info["protocol"] == "CAN"]
UART_PORTS = [dev_info["udp_port"] for dev_name, dev_info in DEVICE_CONFIG_DICT.items() if dev_info["protocol"] == "UART"]
AD_PORTS = [dev_info["udp_port"] for dev_name, dev_info in DEVICE_CONFIG_DICT.items() if dev_info["protocol"] == "AD"]
OC_PORTS = [dev_info["udp_port"] for dev_name, dev_info in DEVICE_CONFIG_DICT.items() if dev_info["protocol"] == "OC"]
TEST_PORTS = [dev_info["udp_port"] for dev_name, dev_info in DEVICE_CONFIG_DICT.items()][:10] # 前10个外设端口
# 自测结果存储
test_result = {
"connect": {"pass": False, "msg": "", "detail": []},
"single_port": {"pass": False, "msg": ""},
"multi_port": {"pass": False, "msg": ""},
"1553b_latency": {"pass": False, "msg": "", "avg": 0, "min": 0, "max": 0},
"10_concurrent": {"pass": False, "msg": "", "send": 0, "recv": 0, "loss_rate": 0},
"can_1553b_order": {"pass": False, "msg": ""}
}
# 初始化日志和报告目录
os.makedirs("test/logs", exist_ok=True)
os.makedirs("test/report", exist_ok=True)
# 自测日志配置
test_logger = logger
test_logger.add("test/logs/udp_auto_test_{time:YYYYMMDDHHmmss}.log", level="INFO", format="{time} | {level} | {message}")
# 报告生成时间
report_time = datetime.now().strftime("%Y%m%d%H%M%S")
class UDPTestClient:
"""UDP测试客户端模拟故障注入软件提供收发基础能力"""
def __init__(self):
self.send_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.send_sock.setblocking(False)
self.recv_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.recv_sock.bind((LOCAL_IP, FAULT_INJECT_PORT))
self.recv_sock.setblocking(False)
self.recv_count = 0
self.recv_latency = []
self.recv_lock = threading.Lock()
def send_hex(self, port, raw_data):
"""发送原始数据转16进制流到指定端口"""
try:
hex_data = binascii.hexlify(raw_data).decode("utf-8")
self.send_sock.sendto(hex_data.encode("utf-8"), (LOCAL_IP, port))
return True
except Exception as e:
test_logger.error(f"端口{port}发送失败:{e}")
return False
def recv_loop(self):
"""后台接收线程,统计接收数和时延"""
while True:
try:
data, addr = self.recv_sock.recvfrom(BUFFER_SIZE)
with self.recv_lock:
self.recv_count += 1
# 解析回传数据,仅统计有效数据
if data:
test_logger.debug(f"接收回传数据:{addr} | {data.hex()[:32]}...")
except BlockingIOError:
time.sleep(0.000001)
continue
except Exception as e:
test_logger.error(f"接收异常:{e}")
continue
def recv_latency_loop(self):
"""时延测试专用接收线程,记录接收时间"""
while True:
try:
data, addr = self.recv_sock.recvfrom(BUFFER_SIZE)
with self.recv_lock:
self.recv_latency.append(time.time())
self.recv_count += 1
except BlockingIOError:
time.sleep(0.000001)
continue
except Exception as e:
test_logger.error(f"时延测试接收异常:{e}")
continue
def reset_recv(self):
"""重置接收统计"""
with self.recv_lock:
self.recv_count = 0
self.recv_latency = []
def close(self):
"""关闭套接字"""
self.send_sock.close()
self.recv_sock.close()
# ---------------------- 自测用例执行 ----------------------
def check_port_listen(ip, port):
"""检测端口是否监听"""
for conn in psutil.net_connections(kind='inet'):
if conn.laddr.ip == ip and conn.laddr.port == port and conn.status == 'LISTEN':
return True
return False
def test_connectivity():
"""用例1UDP端口连接性检测"""
test_logger.info("===== 开始执行UDP端口连接性检测 =====")
detail = []
pass_flag = True
# 检测所有外设端口
for dev_name, dev_info in DEVICE_CONFIG_DICT.items():
port = dev_info["udp_port"]
is_listen = check_port_listen(LOCAL_IP, port)
detail.append(f"{dev_name} | {LOCAL_IP}:{port} | {'✅ 正常' if is_listen else '❌ 未监听'}")
if not is_listen:
pass_flag = False
# 结果存储
test_result["connect"]["pass"] = pass_flag
test_result["connect"]["detail"] = detail
if pass_flag:
test_result["connect"]["msg"] = "所有配置端口均正常监听"
test_logger.info(f"连接性检测通过:{test_result['connect']['msg']}")
else:
test_result["connect"]["msg"] = "存在端口未监听,请检查配置和工程主程序"
test_logger.error(f"连接性检测失败:{test_result['connect']['msg']}")
def test_single_port_trans(client):
"""用例2单端口数据传输测试选Uart0/8880"""
test_logger.info("===== 开始执行:单端口数据传输测试 =====")
if not UART_PORTS:
test_result["single_port"]["msg"] = "未配置UART端口跳过测试"
test_result["single_port"]["pass"] = True
test_logger.warning(test_result["single_port"]["msg"])
return
test_port = UART_PORTS[0]
client.reset_recv()
# 发送10次测试数据
send_ok = 0
for _ in range(10):
if client.send_hex(test_port, TEST_DATA):
send_ok += 1
time.sleep(0.1)
time.sleep(1)
# 验证结果
if send_ok == 10 and client.recv_count >= 1:
test_result["single_port"]["pass"] = True
test_result["single_port"]["msg"] = f"端口{test_port}发送10次成功接收回传{client.recv_count}次,传输正常"
test_logger.info(test_result["single_port"]["msg"])
else:
test_result["single_port"]["pass"] = False
test_result["single_port"]["msg"] = f"端口{test_port}发送成功{send_ok}次,接收回传{client.recv_count}次,传输异常"
test_logger.error(test_result["single_port"]["msg"])
def test_multi_port_trans(client):
"""用例3多端口并行传输测试UART/CAN/1553B/AD/OC各1个"""
test_logger.info("===== 开始执行:多端口并行传输测试 =====")
test_ports = []
if UART_PORTS: test_ports.append(UART_PORTS[0])
if CAN_PORTS: test_ports.append(CAN_PORTS[0])
if B1553B_PORTS: test_ports.append(B1553B_PORTS[0])
if AD_PORTS: test_ports.append(AD_PORTS[0])
if OC_PORTS: test_ports.append(OC_PORTS[0])
if len(test_ports) < 3:
test_result["multi_port"]["msg"] = "协议端口配置不足,跳过测试"
test_result["multi_port"]["pass"] = True
test_logger.warning(test_result["multi_port"]["msg"])
return
# 多线程并行发送
client.reset_recv()
def send_task(port):
for _ in range(5):
client.send_hex(port, TEST_DATA + str(port).encode())
time.sleep(0.05)
threads = [threading.Thread(target=send_task, args=(p,), daemon=True) for p in test_ports]
for t in threads:
t.start()
for t in threads:
t.join()
time.sleep(2)
# 验证结果
if client.recv_count >= len(test_ports):
test_result["multi_port"]["pass"] = True
test_result["multi_port"]["msg"] = f"多端口{test_ports}并行发送完成,接收回传{client.recv_count}次,无串包"
test_logger.info(test_result["multi_port"]["msg"])
else:
test_result["multi_port"]["pass"] = False
test_result["multi_port"]["msg"] = f"多端口并行发送后,仅接收回传{client.recv_count}次,存在串包/丢包"
test_logger.error(test_result["multi_port"]["msg"])
def test_1553b_latency(client):
"""用例41553B时延测试要求6-12us"""
test_logger.info("===== 开始执行1553B时延测试 =====")
if not B1553B_PORTS:
test_result["1553b_latency"]["msg"] = "未配置1553B端口跳过测试"
test_result["1553b_latency"]["pass"] = True
test_logger.warning(test_result["1553b_latency"]["msg"])
return
test_port = B1553B_PORTS[0]
client.reset_recv()
# 启动时延专用接收线程
recv_thread = threading.Thread(target=client.recv_latency_loop, daemon=True)
recv_thread.start()
time.sleep(0.5)
# 发送测试数据,记录发送时间
send_times = []
for _ in range(TEST_TIMES_1553B):
send_ts = time.time()
client.send_hex(test_port, TEST_DATA)
send_times.append(send_ts)
time.sleep(0.000001) # 1us间隔
time.sleep(2)
# 计算时延(微秒)
valid_latency = []
for send_ts, recv_ts in zip(send_times, client.recv_latency):
latency = (recv_ts - send_ts) * 1000000
if 0 < latency < 100: # 过滤异常时延
valid_latency.append(latency)
# 统计结果
if valid_latency:
avg_lat = sum(valid_latency) / len(valid_latency)
min_lat = min(valid_latency)
max_lat = max(valid_latency)
test_result["1553b_latency"]["avg"] = round(avg_lat, 2)
test_result["1553b_latency"]["min"] = round(min_lat, 2)
test_result["1553b_latency"]["max"] = round(max_lat, 2)
if 6 <= avg_lat <= 12:
test_result["1553b_latency"]["pass"] = True
test_result["1553b_latency"]["msg"] = f"平均时延{avg_lat:.2f}us最小{min_lat:.2f}us最大{max_lat:.2f}us满足6-12us要求"
else:
test_result["1553b_latency"]["pass"] = False
test_result["1553b_latency"]["msg"] = f"平均时延{avg_lat:.2f}us超出6-12us要求范围"
else:
test_result["1553b_latency"]["pass"] = False
test_result["1553b_latency"]["msg"] = "未获取有效时延数据,测试失败"
test_logger.info(f"1553B时延测试结果{test_result['1553b_latency']['msg']}")
def test_10_device_concurrent(client):
"""用例510机并发接入测试要求0丢包"""
test_logger.info("===== 开始执行10机并发接入测试 =====")
if len(TEST_PORTS) < 10:
test_result["10_concurrent"]["msg"] = "外设配置不足10个跳过测试"
test_result["10_concurrent"]["pass"] = True
test_logger.warning(test_result["10_concurrent"]["msg"])
return
client.reset_recv()
total_send = 0
# 10个线程对应10个外设
def send_task(port):
nonlocal total_send
for _ in range(TEST_TIMES_CONCURRENT):
client.send_hex(port, TEST_DATA)
total_send += 1
time.sleep(0.0001)
threads = [threading.Thread(target=send_task, args=(p,), daemon=True) for p in TEST_PORTS]
for t in threads:
t.start()
for t in threads:
t.join()
time.sleep(3)
# 计算丢包率
loss_rate = (total_send - client.recv_count) / total_send * 100 if total_send > 0 else 100
test_result["10_concurrent"]["send"] = total_send
test_result["10_concurrent"]["recv"] = client.recv_count
test_result["10_concurrent"]["loss_rate"] = round(loss_rate, 4)
# 验证结果
if loss_rate == 0:
test_result["10_concurrent"]["pass"] = True
test_result["10_concurrent"]["msg"] = f"总发送{total_send}次,总接收{client.recv_count}丢包率0%,满足要求"
else:
test_result["10_concurrent"]["pass"] = False
test_result["10_concurrent"]["msg"] = f"总发送{total_send}次,总接收{client.recv_count}次,丢包率{loss_rate:.4f}%不满足0丢包要求"
test_logger.info(f"10机并发测试结果{test_result['10_concurrent']['msg']}")
def test_can_1553b_order(client):
"""用例6CAN/1553B消息顺序检测人工复核+日志验证)"""
test_logger.info("===== 开始执行CAN/1553B消息顺序检测 =====")
if not CAN_PORTS or not B1553B_PORTS:
test_result["can_1553b_order"]["msg"] = "未配置CAN/1553B端口跳过测试"
test_result["can_1553b_order"]["pass"] = True
test_logger.warning(test_result["can_1553b_order"]["msg"])
return
# 按顺序发送2个CAN+2个1553B端口数据
test_ports = CAN_PORTS[:2] + B1553B_PORTS[:2]
send_order = []
for port in test_ports:
client.send_hex(port, TEST_DATA + f"_ORDER_{port}".encode())
send_order.append(port)
time.sleep(0.5)
# 结果判定(日志人工复核)
test_result["can_1553b_order"]["pass"] = True
test_result["can_1553b_order"]["msg"] = f"已按顺序{send_order}发送数据请查看工程logs/run_*.log验证是否按序接收无乱序即通过"
test_logger.info(test_result["can_1553b_order"]["msg"])
# ---------------------- 报告生成 ----------------------
def generate_test_report():
"""生成可视化自测报告txt+表格)"""
test_logger.info("===== 开始生成UDP自测报告 =====")
# 总通过率
total_cases = len(test_result)
pass_cases = sum(1 for v in test_result.values() if v["pass"])
pass_rate = (pass_cases / total_cases) * 100 if total_cases > 0 else 0
# 生成PrettyTable表格
tb = PrettyTable()
tb.title = f"UDP连接&数据传输自测报告 - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
tb.field_names = ["自测用例", "测试结果", "核心指标/结果描述"]
tb.align = "l"
# 填充用例结果
tb.add_row(["1.端口连接性检测", "✅ 通过" if test_result["connect"]["pass"] else "❌ 失败", test_result["connect"]["msg"]])
tb.add_row(["2.单端口数据传输", "✅ 通过" if test_result["single_port"]["pass"] else "❌ 失败", test_result["single_port"]["msg"]])
tb.add_row(["3.多端口并行传输", "✅ 通过" if test_result["multi_port"]["pass"] else "❌ 失败", test_result["multi_port"]["msg"]])
tb.add_row(["4.1553B时延检测", "✅ 通过" if test_result["1553b_latency"]["pass"] else "❌ 失败",
f"平均{test_result['1553b_latency']['avg']}us | 最小{test_result['1553b_latency']['min']}us | 最大{test_result['1553b_latency']['max']}us | {test_result['1553b_latency']['msg']}"])
tb.add_row(["5.10机并发接入", "✅ 通过" if test_result["10_concurrent"]["pass"] else "❌ 失败",
f"发送{test_result['10_concurrent']['send']} | 接收{test_result['10_concurrent']['recv']} | 丢包率{test_result['10_concurrent']['loss_rate']}% | {test_result['10_concurrent']['msg']}"])
tb.add_row(["6.CAN/1553B消息顺序", "✅ 通过" if test_result["can_1553b_order"]["pass"] else "❌ 失败", test_result["can_1553b_order"]["msg"]])
tb.add_row(["", "", ""])
tb.add_row(["📊 自测总览", f"总用例{total_cases}个 | 通过{pass_cases}个 | 通过率{pass_rate:.2f}%", ""])
# 保存报告到文件
report_path = f"test/report/udp_auto_test_report_{report_time}.txt"
with open(report_path, "w", encoding="utf-8") as f:
f.write(str(tb))
# 追加端口连接性详情
f.write("\n\n=== 端口连接性检测详情 ===\n")
for line in test_result["connect"]["detail"]:
f.write(line + "\n")
# 控制台打印报告
print("\n" + "="*80)
print(tb)
print("="*80)
print(f"\n📋 自测报告已保存至:{os.path.abspath(report_path)}")
print(f"📜 自测日志已保存至test/logs/udp_auto_test_{report_time}.log")
test_logger.info(f"自测报告生成完成,通过率{pass_rate:.2f}%")
# ---------------------- 主执行入口 ----------------------
def main():
print("🚀 虚拟仿真平台中间层软件 - UDP一键自测脚本启动")
print(f"📌 测试时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
print(f"📌 工程UDP配置{LOCAL_IP} | 故障注入端口:{FAULT_INJECT_PORT}")
print(f"📌 测试外设数:{len(DEVICE_CONFIG_DICT)} | 并发测试数:{len(TEST_PORTS)}")
print("-"*50)
# 步骤1检测工程主程序是否启动UDP服务是否初始化
if not udp_server.udp_sockets:
print("❌ 检测到工程UDP服务未初始化请先启动main.py后再执行自测")
test_logger.error("工程UDP服务未初始化自测终止")
exit(1)
# 步骤2初始化测试客户端
try:
client = UDPTestClient()
# 启动通用接收线程
recv_thread = threading.Thread(target=client.recv_loop, daemon=True)
recv_thread.start()
test_logger.info("UDP测试客户端初始化完成启动接收线程")
except Exception as e:
print(f"❌ UDP测试客户端初始化失败{e}")
test_logger.error(f"测试客户端初始化失败:{e}")
exit(1)
# 步骤3按顺序执行所有自测用例
test_connectivity() # 用例1
if test_result["connect"]["pass"]: # 连接性通过后才执行后续用例
test_single_port_trans(client) # 用例2
test_multi_port_trans(client) # 用例3
test_1553b_latency(client) # 用例4
test_10_device_concurrent(client)# 用例5
test_can_1553b_order(client) # 用例6
else:
test_logger.error("端口连接性检测失败,终止后续自测用例")
# 步骤4生成自测报告
generate_test_report()
# 步骤5释放资源
client.close()
udp_server.close()
print("\n✅ UDP一键自测脚本执行完成所有资源已释放")
test_logger.info("UDP一键自测脚本执行完成")
if __name__ == "__main__":
main()

162
tests/udp_test_connect.py Normal file
View File

@@ -0,0 +1,162 @@
'''
功能对等客户端:模拟故障注入软件,发送/接收UDP数据
authorTan Mingyan
更新日期2025/2/26
'''
'''
2. 功能自测步骤
步骤 1启动工程主程序
运行main.py查看日志确认
外设配置成功加载10 个外设);
UDP 服务初始化完成,所有端口正常监听;
无套接字绑定、配置解析错误。
步骤 2运行 UDP 测试客户端
运行test/udp_test_client.py执行单端口发送和多端口并行发送验证以下核心功能
发送成功:客户端打印📤 发送数据成功,无端口连接失败;
中间层接收成功查看工程logs/run_*.log打印对应端口的数据接收日志无 16 进制解析错误;
端口隔离性:多端口并行发送时,各端口数据独立接收,无串包、丢包(日志中各外设数据互不干扰);
校验和检测若发送数据包含正确校验位日志打印校验和检测通过若修改校验位为错误值日志打印校验和检测失败并记录到logs/error_*.log
回传数据接收若在工程中调用udp_server.send_to_fault()(如协议解析后回传),客户端能打印📥 接收中间层回传数据,数据格式正确。
3. 常见问题排查
表格
问题现象 排查方向
客户端提示发送数据失败 1. 中间层主程序是否未启动2. 目标端口是否在 CSV 配置中3. 端口是否被占用
中间层未接收数据 1. 客户端发送的 IP 是否为127.0.0.12. 16 进制流格式是否正确无乱码3. 查看logs/error_*.log是否有数据源过滤非故障注入 IP
校验和检测失败 1. 发送数据的校验位是否与error_check.py的算法匹配2. 数据是否包含校验位(长度是否足够)
多端口串包 1. CSV 配置中是否有重复端口2. 工程udp_handler.py的_get_dev_by_port是否正确映射外设
'''
import socket
import binascii
import threading
import time
import sys
import os
import io
# 设置标准输出和标准错误的编码为UTF-8
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
from pathlib import Path
# 获取当前文件的目录
current_dir = os.path.dirname(os.path.abspath(__file__))
# 获取上级目录(项目根目录)
parent_dir = os.path.dirname(current_dir)
# 将项目根目录添加到系统路径
sys.path.append(parent_dir)
#sys.path.append(str(Path(__file__).parent.parent.parent))
from midware.config.base_config import UDP_CONFIG
class UDPTestClient:
"""UDP测试客户端模拟故障注入软件发送/接收UDP数据"""
def __init__(self):
self.local_ip = UDP_CONFIG["LOCAL_IP"]
self.fault_port = UDP_CONFIG["FAULT_INJECT_PORT"] # 故障注入软件接收端口(客户端监听)
self.buffer_size = UDP_CONFIG["BUFFER_SIZE"]
# 发送套接字(向中间层外设端口发包)
self.send_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.send_sock.setblocking(False)
# 接收套接字(监听故障注入端口,接收中间层回传)
self.recv_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.recv_sock.bind((self.local_ip, self.fault_port))
self.recv_sock.setblocking(False)
# 接收线程标识
self.recv_running = True
# 启动接收线程(后台接收,不阻塞发包)
self.recv_thread = threading.Thread(target=self._recv_loop, daemon=True)
self.recv_thread.start()
print(f"UDP测试客户端初始化完成 | 监听回传数据:{self.local_ip}:{self.fault_port}")
def _recv_loop(self):
"""后台接收线程接收中间层回传的16进制流数据"""
while self.recv_running:
try:
data, addr = self.recv_sock.recvfrom(self.buffer_size)
hex_data = data.decode("utf-8").strip()
raw_data = binascii.unhexlify(hex_data) if hex_data else b""
print(f"\n📥 接收中间层回传数据 | 来源:{addr} | 16进制流{hex_data[:64]}... | 原始数据长度:{len(raw_data)}字节")
except BlockingIOError:
time.sleep(0.001)
continue
except Exception as e:
print(f"\n❌ 接收数据异常:{str(e)}")
continue
def send_hex_data(self, dev_port, raw_data):
"""
向中间层指定外设端口发送16进制流数据模拟故障注入软件发包
:param dev_port: 外设UDP端口中间层监听端口
:param raw_data: 原始二进制数据自动转为16进制流
"""
try:
# 原始数据转16进制流符合工程数据格式要求
hex_data = binascii.hexlify(raw_data).decode("utf-8")
self.send_sock.sendto(hex_data.encode("utf-8"), (self.local_ip, dev_port))
print(f"\n📤 发送数据成功 | 目标端口:{dev_port} | 16进制流{hex_data[:64]}... | 原始数据长度:{len(raw_data)}字节")
return True
except Exception as e:
print(f"\n❌ 发送数据失败 | 目标端口:{dev_port} | 异常:{str(e)}")
return False
def close(self):
"""关闭套接字,释放资源"""
self.recv_running = False
self.send_sock.close()
self.recv_sock.close()
print("\n✅ UDP测试客户端已关闭")
# 多端口并行发送测试(线程版)
def multi_port_send(client, port_data_list):
"""
多端口并行发送数据,验证端口隔离性
:param port_data_list: 列表,元素为(端口, 原始数据)
"""
def send_task(port, data):
client.send_hex_data(port, data)
# 模拟不同单机的发包间隔
time.sleep(0.001)
threads = []
for port, data in port_data_list:
t = threading.Thread(target=send_task, args=(port, data), daemon=True)
threads.append(t)
t.start()
# 等待所有线程完成
for t in threads:
t.join()
print("\n===== 多端口并行发送任务完成 =====")
if __name__ == "__main__":
# 初始化测试客户端
client = UDPTestClient()
# 测试用原始数据可自定义建议包含校验位适配error_check.py的校验逻辑
test_data1 = b"UART_TEST_DATA_001" + b'\x9c\x47' # UART数据+CRC16校验位
test_data2 = b"CAN_TEST_DATA_001" + b'\x1a\x2b' # CAN数据+CRC16校验位
test_data3 = b"1553B_TEST_DATA_001" + b'\x0f\x3d' # 1553B数据+CRC16校验位
test_data4 = b"AD_TEST_DATA_001" + b'\x5f' # AD数据+累加和校验位
test_data5 = b"OC_TEST_DATA_001" + b'\x6a' # OC数据+累加和校验位
try:
# 1. 单端口发送测试向Uart0(8880)发送数据)
print("===== 单端口发送测试 =====")
# client.send_hex_data(8880, test_data1)
client.send_hex_data(4000, test_data1)
time.sleep(1)
# 2. 多端口并行发送测试(验证端口互不干扰)
print("\n===== 多端口并行发送测试 =====")
port_data = [
(4000, test_data1), (4001, test_data2), (4002, test_data3),
(4003, test_data4), (4004, test_data1)
]
multi_port_send(client, port_data)
time.sleep(3)
except KeyboardInterrupt:
print("\n\n用户终止测试")
finally:
# 关闭客户端
client.close()
time.sleep(1)