Files
HW-Debug-Tools/SerialMate/src/selftest_gui.py
T

264 lines
11 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.
"""GUI 功能自检(offscreen 无窗口): 组帧预览/TX回显/帧日志/通道提取/显示切换/
未连接丢弃/配置往返
====================================================================
不依赖硬件与串口, 直接驱动 Window 的事件处理函数; 改 main.py 后建议重跑。
用法: python selftest_gui.py (全部通过退出码 0)
"""
import os
import re
import sys
os.environ['QT_QPA_PLATFORM'] = 'offscreen'
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from PySide6.QtWidgets import QApplication
import config
import main as m
import protocol as d
from plot import fmt_val
fails = []
def check(name, got, want):
ok = got == want
print(('PASS' if ok else 'FAIL'), name, '->', repr(got)[:120],
'' if ok else f'(期望 {want!r})')
if not ok:
fails.append(name)
# 0) 数值显示格式: 普通小数千分位(科学计数只留给 10 亿以上)
check('fmt_val 千分位', fmt_val(28210029), '28,210,029')
check('fmt_val 小数', fmt_val(24.6), '24.6')
check('fmt_val 负小数', fmt_val(-0.25), '-0.25')
check('fmt_val 零', fmt_val(0), '0')
app = QApplication(sys.argv)
win = m.Window()
win.show() # offscreen 显示, isVisible 类断言前提
check('倍率/偏移/序号框宽度(防数值裁切回归)',
win._ch_ui[0]['scale'].width() >= 130
and win._ch_ui[0]['off'].width() >= 110
and win._ch_ui[0]['idx'].width() >= 110, True)
win._apply_minimum_size()
check('窗口最小高度(防曲线卡叠压)', win.minimumHeight() >= 800, True)
# 0b) 曲线时间窗换算(纯计算; 窗口不向数据收敛——选 5min 就显示 5min 宽的轴)
p = win.plot
p.set_view(10)
p.set_offset(0)
check('时间窗跟随右缘', p.visible_domain(0, 100) == (90.0, 100.0), True)
p.set_offset(30)
check('时间窗滞后回看', p.visible_domain(0, 100) == (60.0, 70.0), True)
p.set_offset(90)
check('时间窗滑到最旧', p.visible_domain(0, 100) == (0.0, 10.0), True)
p.set_offset(500)
check('滞后超史为空窗(保宽)', p.visible_domain(0, 100) == (-410.0, -400.0), True)
p.set_view(300)
p.set_offset(0)
check('数据不足窗宽不收敛', p.visible_domain(0, 23) == (-277.0, 23.0), True)
p.set_view(None)
p.set_offset(0)
check('跟随全部', p.visible_domain(0, 100) == (0.0, 100.0), True)
# 0c) 悬停读值: 最近采样命中/容差外未命中
import time as _time
base = _time.perf_counter()
p._times[0].clear()
p._vals[0].clear()
p._names[0] = '甲'
for tt, vv in ((0.0, 10.0), (1.0, 20.0), (2.0, 30.0)):
p._times[0].append(base + tt)
p._vals[0].append(vv)
check('悬停命中最近点', p._nearest(0, base + 1.9, 0.2) == ('甲', 30.0), True)
check('悬停容差外未命中', p._nearest(0, base + 1.4, 0.2) is None, True)
lo, hi = p.auto_yrange()
check('自动Y量程', abs(lo - 8.4) < 1e-9 and abs(hi - 31.6) < 1e-9, True)
from PySide6.QtCore import QPoint
p.resize(600, 300) # 未走布局, 手动给几何
lines = p._hover_lines_at(QPoint(320, 150)) # x=绘图区正中 → t=base+1.0
check('悬停框内容(位置固定也随数据刷新)',
lines[:2] == [f'T = {base + 1.0:.2f}s', '甲 = 20'], True)
# 0d) 暂停渲染 vs 暂停采集: 冻结期间继续收数, 解冻后数据都在
p.set_frozen(True)
n0 = len(p._times[0])
p.add(0, 42.0)
check('冻结期间继续收数', p.is_frozen() and len(p._times[0]) == n0 + 1, True)
p.set_frozen(False)
p.set_paused(True)
n1 = len(p._times[0])
p.add(0, 43.0)
check('暂停期间丢弃', p.is_paused() and len(p._times[0]) == n1, True)
p.set_paused(False)
# 0e) 时间戳: 纯格式化函数, 传入到达时刻得确定字符串, 不传兜底当前时刻
t_fix = 1759474800 + 0.456
check('时间戳格式化(到达时刻)', m._timestamp(t_fix),
_time.strftime('%H:%M:%S', _time.localtime(t_fix)) + '.456')
check('时间戳格式化(现在兜底)',
re.match(r'\d{2}:\d{2}:\d{2}\.\d{3}$', m._timestamp()) is not None, True)
# 1) 组帧预览: 多条 '/' 分隔
win.ed_tx.setText('M1/M731S0T1000')
check('多条预览', win.lb_preview.text(),
'D5 01 02 4D 31 81 | D5 01 0B 4D 37 33 31 53 30 54 31 30 30 30 8C')
win.combo_tx_mode.setCurrentIndex(1)
win.ed_tx.setText('D5 01 02 4D 31 81')
check('HEX预览', win.lb_preview.text(), '→ D5 01 02 4D 31 81')
win.combo_tx_mode.setCurrentIndex(0)
win.ed_tx.setText('M1')
# 1b) 帧构成定制: 无板ID布局 + LEN 口径切换 + 非法输入提示
win.ed_layout.setText('HDR LEN PAYLOAD CRC')
check('帧构成无ID预览', win.lb_preview.text(), 'D5 02 4D 31 80')
win.combo_len_sem.setCurrentIndex(1) # 到帧尾(payload+CRC)
check('帧构成LEN口径预览', win.lb_preview.text(), 'D5 03 4D 31 81')
win.combo_len_sem.setCurrentIndex(0)
win.ed_layout.setText('HDR LEN') # 缺 PAYLOAD/CRC -> 拒绝
check('帧构成非法提示', win.lb_preview.text().startswith('⚠'), True)
check('帧构成非法详情', 'PAYLOAD 必须恰好出现一次' in win.lb_proto.text(), True)
win.ed_layout.setText(d.DEFAULT_LAYOUT)
check('帧构成还原预览', win.lb_preview.text(), 'D5 01 02 4D 31 81')
# 2) TX 回显行(直接驱动处理函数, 断言前 flush 批量日志缓冲)
cfg = win._proto_from_ui()
win._on_tx('M1', cfg.build_frame(b'M1'))
win._flush_rx()
check('TX日志行', 'M1 [D5 01 02 4D 31 81]' in win.text_rx.toPlainText(), True)
# 3) 帧事件: 日志 + 通道提取(文本数字×0.1)
win.combo_display.setCurrentIndex(0)
fr = d.FrameParser(d.ProtoConfig()).feed(
bytes.fromhex('B5 F0 08 6F 6B 54 4C 3A 32 34 36 48'))[0]
win._ch_ui[0]['chk'].setChecked(True)
win._ch_ui[0]['mode'].setCurrentIndex(1) # 文本数字
win._ch_ui[0]['idx'].setValue(1)
win._ch_ui[0]['scale'].setValue(0.1)
win._on_frame(fr)
win._flush_rx()
last = win.text_rx.toPlainText().splitlines()[-1]
check('帧日志含 TYPE 名', 'B=0xF0 成功' in last and 'LEN=8' in last
and 'okTL:246' in last, True)
check('通道最新值', win._ch_ui[0]['val'].text(), '24.6')
# 时间戳用 link 后台线程打的到达时刻, 而非 GUI 消费时刻
ft = _time.time() - 3.456
win._on_frame(fr, ft)
win._flush_rx()
check('帧日志用到达时刻', m._timestamp(ft)
in win.text_rx.toPlainText().splitlines()[-1], True)
# 4) HEX 显示切换 + 原始帧
win.combo_display.setCurrentIndex(1)
win._on_frame(fr)
win._flush_rx()
last = win.text_rx.toPlainText().splitlines()[-1]
check('HEX显示+原始帧', '6F 6B 54 4C 3A 32 34 36' in last
and '[B5 F0 08 6F 6B 54 4C 3A 32 34 36 48]' in last, True)
# 5) 透传行
win.combo_display.setCurrentIndex(1)
win._on_raw(b'\x11\x22')
win._flush_rx()
check('透传HEX行', win.text_rx.toPlainText().splitlines()[-1].endswith('11 22'), True)
# 6) 未连接发送丢弃
win._send()
win._drain_events()
check('未连接发送提示', '未连接串口, 命令被丢弃' in win.text_rx.toPlainText(), True)
# 6b) 通道配置缓存: 帧路径读 _ch_cfg, 用户改配置时重建
win._ch_ui[1]['chk'].setChecked(True)
win._ch_ui[1]['name'].setText('乙')
check('通道缓存随配置重建', win._ch_cfg[1]['enabled']
and win._ch_cfg[1]['name'] == '乙', True)
win._ch_ui[1]['chk'].setChecked(False)
check('通道缓存关闭同步', not win._ch_cfg[1]['enabled'], True)
# 6c) 积压折叠: 队列积压超阈值时帧日志折叠为计数摘要(带首尾到达时刻), 曲线照常入库
rx_n0, plot_n0 = win._rx_n, len(p._vals[0])
now = _time.time()
for k in range(250): # 100ms 间隔的到达时刻, 模拟高频上报
win.events.put(('frame', fr, now - (250 - k) / 10.0))
win._drain_events()
log = win.text_rx.toPlainText()
check('积压折叠摘要行', '折叠 250 帧' in log, True)
check('折叠显示到达跨度', m._timestamp(now - 25.0) in log
and m._timestamp(now - 0.1) in log, True)
check('折叠保曲线', len(p._vals[0]) == plot_n0 + 250, True)
check('折叠计数器照常', win._rx_n == rx_n0 + 250, True)
check('折叠摘要含帧描述', 'B=0xF0 成功 ×250' in log, True)
# 7) 配置往返
store = win._store_from_ui()
check('配置 proto', (store['proto']['tx_hdr'], store['proto']['crc_algo']),
('D5', 'SUM8'))
check('配置帧构成', (store['proto']['layout'], store['proto']['len_sem']),
(d.DEFAULT_LAYOUT, 'payload'))
check('配置通道1', (store['channels'][0]['enabled'], store['channels'][0]['scale']),
(True, 0.1))
# 8) 板型档案: 保存/切换应用协议参数/存入命令/清理
check('板型档案加载', win.combo_profile.count() >= 1, True)
win.ed_board.setText('03')
check('存为板型', win._save_profile_as('测试板'), True)
names = [win.combo_profile.itemText(i) for i in range(win.combo_profile.count())]
check('板型入列', '测试板' in names, True)
pidx = names.index('测试板')
win.combo_profile.setCurrentIndex(pidx)
check('切换板型应用板ID', win.ed_board.text(), '03')
win.combo_profile.setCurrentIndex(0)
check('切回默认板ID', win.ed_board.text(), '01')
win.ed_tx.setText('M1')
win._add_command_named('M1', 'M1')
check('存入命令去重', any(c[1] == 'M1' for c in win._commands)
and len([c for c in win._commands if c[1] == 'M1']) == 1, True)
check('预设显示名命令', win.combo_preset.itemText(0) == '握手 · M1', True)
# 10) 新建板型(命令预设从空白开始) + 命令预设表编辑器
check('新建板型', win._new_profile_named('新建测试板'), True)
check('重名新建拒绝', win._new_profile_named('新建测试板'), False)
check('新建板型命令为空', win._commands == [], True)
dlg = m.ProfileEditDialog(win.profiles[win.combo_profile.currentIndex()])
from PySide6.QtWidgets import QTableWidgetItem
for r, (n, c) in enumerate((('甲', 'A1'), ('乙', 'B2'), ('丙', 'C3'))):
dlg.table._add_row()
dlg.table.setItem(r, 1, QTableWidgetItem(n))
dlg.table.setItem(r, 2, QTableWidgetItem(c))
check('编辑器收集', dlg.commands() == [['甲', 'A1'], ['乙', 'B2'], ['丙', 'C3']],
True)
dlg.table.move_row(0, 2) # 拖动排序: 首行挪到末尾
check('拖动换序', dlg.commands() == [['乙', 'B2'], ['丙', 'C3'], ['甲', 'A1']],
True)
check('手柄列存在', dlg.table.item(0, 0).text() == '≡', True)
dlg.table._add_row() # 重复命令与空命令行均被忽略
dlg.table.setItem(3, 1, QTableWidgetItem('重复'))
dlg.table.setItem(3, 2, QTableWidgetItem('B2'))
check('编辑器去重空行', dlg.commands() == [['乙', 'B2'], ['丙', 'C3'], ['甲', 'A1']],
True)
# 11) 删除板型: 核函数删除/不存在拒绝/至少保留一个
check('删除板型', win._delete_profile('新建测试板'), True)
check('删除后不在列', all(p['name'] != '新建测试板' for p in win.profiles), True)
check('删除不存在拒绝', win._delete_profile('不存在的板型'), False)
# 清理运行态测试板型
win.profiles[:] = [p for p in win.profiles if p['name'] != '测试板']
config.save_profiles(win.profiles)
win._reload_profiles()
check('清理测试板型', all(p['name'] != '测试板' for p in win.profiles), True)
# 9) 曲线弹出/嵌回
win._toggle_plot_window()
check('曲线弹出', win._plot_win is not None
and win.plot.window() is win._plot_win, True)
win._toggle_plot_window()
check('曲线嵌回', win._plot_win is None and win.plot.parent() is not None, True)
win._pop_plot()
win._dock_plot()
check('弹出后关窗嵌回', win._plot_win is None and win.plot.isVisible(), True)
win.link.close()
print('RESULT:', 'FAIL:' + ','.join(fails) if fails else 'ALL PASS')
sys.exit(1 if fails else 0)