上一篇我介绍了这个 UFS 测试工具 的整体骨架:
- WinForms 负责界面承载
IUfsAdapter隔离底层厂商实现UfsService负责会话和业务封装ITestCase + TestRunner组成可扩展测试引擎TestController + ViewModel负责测试流程和界面协作
这一篇我不再重复讲分层,而是重点聊:这套骨架接下来应该怎么长,才能从 Demo 骨架变成真正能持续演进的工程工具。
一、骨架已经解决了什么问题?
很多人看到"骨架"两个字,会觉得它只是代码拆得更漂亮一些。
但对测试工具来说,骨架真正解决的是"后续修改成本"。
当前这套结构,已经先把最容易失控的几个点卡住了:
- 厂商差异被隔离 :底层都收敛到
IUfsAdapter - 测试项可插拔 :新增用例只要实现
ITestCase - 会话统一管理 :由
TestRunner负责打开和关闭设备 - 取消链路已打通 :从 UI 到测试执行可以传递
CancellationToken - 结果对象统一 :所有测试最终都回到
TestResult
这意味着后面的迭代,不需要再推翻已有结构,而是在现有边界上不断补能力。
如果要继续演进,我觉得有两张图值得反复看,因为它们直接决定了后面应该优先补什么。
首先是依赖注入关系图:
#mermaid-svg-nx5yTe9Ib5mdLmkW{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-nx5yTe9Ib5mdLmkW .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-nx5yTe9Ib5mdLmkW .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-nx5yTe9Ib5mdLmkW .error-icon{fill:#552222;}#mermaid-svg-nx5yTe9Ib5mdLmkW .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-nx5yTe9Ib5mdLmkW .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-nx5yTe9Ib5mdLmkW .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-nx5yTe9Ib5mdLmkW .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-nx5yTe9Ib5mdLmkW .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-nx5yTe9Ib5mdLmkW .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-nx5yTe9Ib5mdLmkW .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-nx5yTe9Ib5mdLmkW .marker{fill:#333333;stroke:#333333;}#mermaid-svg-nx5yTe9Ib5mdLmkW .marker.cross{stroke:#333333;}#mermaid-svg-nx5yTe9Ib5mdLmkW svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-nx5yTe9Ib5mdLmkW p{margin:0;}#mermaid-svg-nx5yTe9Ib5mdLmkW .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-nx5yTe9Ib5mdLmkW .cluster-label text{fill:#333;}#mermaid-svg-nx5yTe9Ib5mdLmkW .cluster-label span{color:#333;}#mermaid-svg-nx5yTe9Ib5mdLmkW .cluster-label span p{background-color:transparent;}#mermaid-svg-nx5yTe9Ib5mdLmkW .label text,#mermaid-svg-nx5yTe9Ib5mdLmkW span{fill:#333;color:#333;}#mermaid-svg-nx5yTe9Ib5mdLmkW .node rect,#mermaid-svg-nx5yTe9Ib5mdLmkW .node circle,#mermaid-svg-nx5yTe9Ib5mdLmkW .node ellipse,#mermaid-svg-nx5yTe9Ib5mdLmkW .node polygon,#mermaid-svg-nx5yTe9Ib5mdLmkW .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-nx5yTe9Ib5mdLmkW .rough-node .label text,#mermaid-svg-nx5yTe9Ib5mdLmkW .node .label text,#mermaid-svg-nx5yTe9Ib5mdLmkW .image-shape .label,#mermaid-svg-nx5yTe9Ib5mdLmkW .icon-shape .label{text-anchor:middle;}#mermaid-svg-nx5yTe9Ib5mdLmkW .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-nx5yTe9Ib5mdLmkW .rough-node .label,#mermaid-svg-nx5yTe9Ib5mdLmkW .node .label,#mermaid-svg-nx5yTe9Ib5mdLmkW .image-shape .label,#mermaid-svg-nx5yTe9Ib5mdLmkW .icon-shape .label{text-align:center;}#mermaid-svg-nx5yTe9Ib5mdLmkW .node.clickable{cursor:pointer;}#mermaid-svg-nx5yTe9Ib5mdLmkW .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-nx5yTe9Ib5mdLmkW .arrowheadPath{fill:#333333;}#mermaid-svg-nx5yTe9Ib5mdLmkW .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-nx5yTe9Ib5mdLmkW .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-nx5yTe9Ib5mdLmkW .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-nx5yTe9Ib5mdLmkW .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-nx5yTe9Ib5mdLmkW .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-nx5yTe9Ib5mdLmkW .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-nx5yTe9Ib5mdLmkW .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-nx5yTe9Ib5mdLmkW .cluster text{fill:#333;}#mermaid-svg-nx5yTe9Ib5mdLmkW .cluster span{color:#333;}#mermaid-svg-nx5yTe9Ib5mdLmkW div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-nx5yTe9Ib5mdLmkW .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-nx5yTe9Ib5mdLmkW rect.text{fill:none;stroke-width:0;}#mermaid-svg-nx5yTe9Ib5mdLmkW .icon-shape,#mermaid-svg-nx5yTe9Ib5mdLmkW .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-nx5yTe9Ib5mdLmkW .icon-shape p,#mermaid-svg-nx5yTe9Ib5mdLmkW .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-nx5yTe9Ib5mdLmkW .icon-shape .label rect,#mermaid-svg-nx5yTe9Ib5mdLmkW .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-nx5yTe9Ib5mdLmkW .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-nx5yTe9Ib5mdLmkW .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-nx5yTe9Ib5mdLmkW :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Host DI 容器
构造函数注入
构造函数注入
构造函数注入
构造函数注入
IEnumerable注入
IEnumerable注入
构造函数注入
构造函数注入
构造函数注入
IUfsAdapter → VendorUfsAdapter
(Singleton)
Form1
(Transient)
MainViewModel
(Transient)
TestController
(Singleton)
TestRunner
(Transient)
UfsService
(Transient)
ITestCase → ReadWriteTest
(Transient)
ITestCase → StressTest
(Transient)
从这张图里你能很容易看出,后面如果要扩测试项、替换 Adapter、调整 ViewModel 和流程层,改动入口其实已经预留好了。
其次是 Session 生命周期图:
#mermaid-svg-n3aSaD1qqX35kart{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-n3aSaD1qqX35kart .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-n3aSaD1qqX35kart .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-n3aSaD1qqX35kart .error-icon{fill:#552222;}#mermaid-svg-n3aSaD1qqX35kart .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-n3aSaD1qqX35kart .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-n3aSaD1qqX35kart .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-n3aSaD1qqX35kart .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-n3aSaD1qqX35kart .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-n3aSaD1qqX35kart .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-n3aSaD1qqX35kart .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-n3aSaD1qqX35kart .marker{fill:#333333;stroke:#333333;}#mermaid-svg-n3aSaD1qqX35kart .marker.cross{stroke:#333333;}#mermaid-svg-n3aSaD1qqX35kart svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-n3aSaD1qqX35kart p{margin:0;}#mermaid-svg-n3aSaD1qqX35kart defs #statediagram-barbEnd{fill:#333333;stroke:#333333;}#mermaid-svg-n3aSaD1qqX35kart g.stateGroup text{fill:#9370DB;stroke:none;font-size:10px;}#mermaid-svg-n3aSaD1qqX35kart g.stateGroup text{fill:#333;stroke:none;font-size:10px;}#mermaid-svg-n3aSaD1qqX35kart g.stateGroup .state-title{font-weight:bolder;fill:#131300;}#mermaid-svg-n3aSaD1qqX35kart g.stateGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-n3aSaD1qqX35kart g.stateGroup line{stroke:#333333;stroke-width:1;}#mermaid-svg-n3aSaD1qqX35kart .transition{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-n3aSaD1qqX35kart .stateGroup .composit{fill:white;border-bottom:1px;}#mermaid-svg-n3aSaD1qqX35kart .stateGroup .alt-composit{fill:#e0e0e0;border-bottom:1px;}#mermaid-svg-n3aSaD1qqX35kart .state-note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-n3aSaD1qqX35kart .state-note text{fill:black;stroke:none;font-size:10px;}#mermaid-svg-n3aSaD1qqX35kart .stateLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-n3aSaD1qqX35kart .edgeLabel .label rect{fill:#ECECFF;opacity:0.5;}#mermaid-svg-n3aSaD1qqX35kart .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-n3aSaD1qqX35kart .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-n3aSaD1qqX35kart .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-n3aSaD1qqX35kart .edgeLabel .label text{fill:#333;}#mermaid-svg-n3aSaD1qqX35kart .label div .edgeLabel{color:#333;}#mermaid-svg-n3aSaD1qqX35kart .stateLabel text{fill:#131300;font-size:10px;font-weight:bold;}#mermaid-svg-n3aSaD1qqX35kart .node circle.state-start{fill:#333333;stroke:#333333;}#mermaid-svg-n3aSaD1qqX35kart .node .fork-join{fill:#333333;stroke:#333333;}#mermaid-svg-n3aSaD1qqX35kart .node circle.state-end{fill:#9370DB;stroke:white;stroke-width:1.5;}#mermaid-svg-n3aSaD1qqX35kart .end-state-inner{fill:white;stroke-width:1.5;}#mermaid-svg-n3aSaD1qqX35kart .node rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-n3aSaD1qqX35kart .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-n3aSaD1qqX35kart #statediagram-barbEnd{fill:#333333;}#mermaid-svg-n3aSaD1qqX35kart .statediagram-cluster rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-n3aSaD1qqX35kart .cluster-label,#mermaid-svg-n3aSaD1qqX35kart .nodeLabel{color:#131300;}#mermaid-svg-n3aSaD1qqX35kart .statediagram-cluster rect.outer{rx:5px;ry:5px;}#mermaid-svg-n3aSaD1qqX35kart .statediagram-state .divider{stroke:#9370DB;}#mermaid-svg-n3aSaD1qqX35kart .statediagram-state .title-state{rx:5px;ry:5px;}#mermaid-svg-n3aSaD1qqX35kart .statediagram-cluster.statediagram-cluster .inner{fill:white;}#mermaid-svg-n3aSaD1qqX35kart .statediagram-cluster.statediagram-cluster-alt .inner{fill:#f0f0f0;}#mermaid-svg-n3aSaD1qqX35kart .statediagram-cluster .inner{rx:0;ry:0;}#mermaid-svg-n3aSaD1qqX35kart .statediagram-state rect.basic{rx:5px;ry:5px;}#mermaid-svg-n3aSaD1qqX35kart .statediagram-state rect.divider{stroke-dasharray:10,10;fill:#f0f0f0;}#mermaid-svg-n3aSaD1qqX35kart .note-edge{stroke-dasharray:5;}#mermaid-svg-n3aSaD1qqX35kart .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-n3aSaD1qqX35kart .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-n3aSaD1qqX35kart .statediagram-note text{fill:black;}#mermaid-svg-n3aSaD1qqX35kart .statediagram-note .nodeLabel{color:black;}#mermaid-svg-n3aSaD1qqX35kart .statediagram .edgeLabel{color:red;}#mermaid-svg-n3aSaD1qqX35kart #dependencyStart,#mermaid-svg-n3aSaD1qqX35kart #dependencyEnd{fill:#333333;stroke:#333333;stroke-width:1;}#mermaid-svg-n3aSaD1qqX35kart .statediagramTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-n3aSaD1qqX35kart :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 程序启动
TestRunner.RunAllAsync()
调用 OpenSession()
逐个执行 TestCase
执行下一个 TestCase
全部 TestCase 执行完毕
或发生异常
调用 CloseSession()
用户点击取消
finally 块中
调用 CloseSession()
资源释放完毕
未初始化
已初始化
运行中
已关闭
已取消
这张图对应的是整个工具后续最重要的一条原则:设备生命周期一定要收口管理,不要再散回每个测试项里。
二、第一步:把 VendorUfsAdapter 从占位类变成真实接入层
当前 VendorUfsAdapter 里的逻辑还是示意性的:
Initialize()直接返回成功Read()返回固定大小的 byte 数组Write()默认返回trueGetDeviceInfo()返回一个固定字符串
这是非常合理的第一阶段做法,因为它能先把流程跑通。
但要进入真实工程阶段,Adapter 层至少要补 4 件事。
1)明确 SDK 封装边界
如果厂商提供的是 DLL,通常建议不要把 P/Invoke 直接散落在 VendorUfsAdapter 业务逻辑里。
更推荐拆成两层:
text
VendorUfsAdapter
-> VendorSdkClient
-> DLL / IOCTL / Native API
这样做的好处是:
VendorUfsAdapter仍然是面向业务语义的- 真正的 native 调用集中在更底层
- 后期排查问题时,更容易区分"SDK 调用失败"还是"业务流程判断有误"
2)统一错误码与异常模型
真实硬件接入后,很少会只有一个 true/false。
你大概率会遇到:
- 超时
- 设备未就绪
- 读写失败
- 参数非法
- 权限不足
- 驱动未安装
- 设备拔出
如果这些情况直接原样抛给上层,UI 和测试项会很快被错误分支淹没。
更稳的方式是:
- Adapter 层识别底层错误码
- 转换成更一致的异常或结果对象
- Service / TestCase 层只处理统一语义
例如:
UfsDeviceNotReadyExceptionUfsIoTimeoutExceptionUfsWriteFailedException
这样日志、提示、统计都会更清晰。
3)补全设备信息与能力探测
GetDeviceInfo() 不应该只返回一个字符串。
在真实项目中,通常值得扩展出更多信息,比如:
- 厂商名
- 型号
- 容量
- 固件版本
- LUN 数量
- Sector Size
- 是否支持某些特性
如果不想一开始就大改接口,也可以先新增一个信息对象:
csharp
public class UfsDeviceInfo
后面界面显示、报告输出、兼容性判断都会用得到。
4)把资源释放做扎实
占位阶段的 Dispose() 很简单,但真接 SDK 后一定要认真处理:
- 句柄关闭
- 缓冲区释放
- 通讯上下文销毁
- 重复释放保护
底层释放不彻底,是测试工具里非常常见但又很难查的一类问题。
三、第二步:让测试项从"能跑"升级成"可配置"
现在的测试用例是硬编码的,例如:
ReadWriteTest默认写LBA 0StressTest默认循环1000次- 数据块大小固定为
512
这在骨架阶段没问题,但工程里会很快不够用。
因为不同测试场景下,你需要调的参数非常多。
常见可配置项包括:
- 起始 LBA
- Sector Count
- 循环次数
- 随机种子
- 写入模式(固定值 / 递增 / 随机)
- 校验方式(全量比对 / CRC / 抽样)
- 每轮间隔时间
- 超时阈值
我建议的演进方式
不要一上来把所有参数都塞进 RunAsync(...)。
更推荐两种方式之一:
方式 1:引入统一配置对象
例如:
csharp
public class TestExecutionOptions
{
public int StartLba { get; set; }
public int SectorCount { get; set; }
public int LoopCount { get; set; }
}
然后由 TestController 或 TestRunner 在执行前把参数传给测试项。
方式 2:每个测试项有独立参数对象
例如:
ReadWriteTestOptionsStressTestOptionsPerformanceTestOptions
这种方式更适合测试种类差异很大的项目。
如果你的工具后面会做成正式 GUI,我更推荐第二种,因为它更利于和界面配置项一一对应。
四、第三步:给测试结果加更多"工程信息"
当前的 TestResult 已经有:
TestNamePassedErrorMessageDurationTimestamp
这作为最小闭环已经够了。
但真到团队协作阶段,这些信息通常还不够支撑复盘。
我建议补充这些字段:
Category:测试类别,例如功能、压力、性能StepLogs:关键步骤日志摘要StartLba/EndLba:本次操作范围BytesTransferred:读写数据量RetryCount:重试次数DeviceInfoSnapshot:执行时设备快照ErrorCode:底层错误码ExtraData:扩展信息字典
这样做的价值是:
- 结果更适合导出成报告
- 出问题时更容易定位上下文
- 不同测试项之间的横向对比也更自然
尤其是压力测试和性能测试,没有上下文信息的"成功/失败"其实意义有限。
五、第四步:把执行器从"串行跑完"升级成"可编排"
现在 TestRunner 的逻辑比较纯粹:
- 打开 Session
- 依次遍历
ITestCase - 收集结果
- finally 关闭 Session
这已经很好了,但如果你要继续做强,后面可以考虑引入"编排能力"。
可演进方向 1:支持测试分组
例如:
- 基础功能组
- 稳定性组
- 性能组
- 工厂产测组
这样用户可以按场景选择,而不是每次都跑全量。
可演进方向 2:支持前置/后置动作
比如某些测试开始前需要:
- 清空指定区域
- 检查设备状态
- 做一次重新初始化
测试结束后还可能需要:
- 导出日志
- 保存原始数据
- 恢复设备配置
这些都可以逐步沉淀为 Runner 的可扩展管线。
可演进方向 3:支持失败策略
不同团队对失败后的处理策略不一样:
- 遇到失败立即停止
- 单项失败但继续跑后续测试
- 自动重试若干次
- 失败后自动采集更多诊断信息
这些策略如果提前留好接口,工具会更实用。
六、第五步:让取消、进度、状态反馈更完整
你现在已经通过 CancellationToken 打通了取消链路,这一点非常好。
下一步最值得补的是"进度可视化"。
因为真实长时测试里,用户最怕的不是慢,而是不知道它在干什么。
当前可以继续增强的点:
- 总体进度:当前第几个测试 / 共几个测试
- 单项进度:例如压力测试已经执行到第几轮
- 当前动作:正在初始化、正在写入、正在校验、正在收尾
- 预计剩余时间:尤其对长稳测试很有帮助
- 取消中状态:点了取消后告诉用户系统正在回收资源
技术上怎么做?
比较自然的方式有两种:
1)事件通知
由 TestRunner / ITestCase 向外抛进度事件。
2)进度接口
例如引入:
csharp
IProgress<TestProgressInfo>
这样 UI、日志系统、导出器都可以订阅同一份进度信息。
如果后面要做更丰富的界面,这一步非常值。
七、第六步:结果别只显示在 ListBox 里,要能沉淀成资产
现在 Form1 里已经可以把结果展示到列表中,这对演示和基础使用足够了。
但如果要进入真实项目场景,结果一定要能"带走"。
推荐的结果输出方向
1)文本日志
最低成本,便于快速追查。
2)CSV / Excel
适合测试人员横向比对、筛选、统计。
3)JSON
适合自动化系统二次消费。
4)HTML 报告
适合直接给团队、领导、客户看,阅读门槛低。
为什么结果持久化很重要?
因为测试工具的价值,不只是"当下跑完告诉你对不对",更在于:
- 能不能追溯
- 能不能复盘
- 能不能横向比较不同设备/版本
- 能不能沉淀成回归数据
很多时候,一份报告比一次弹窗更有价值。
八、第七步:界面层建议别急着复杂化,但要提前预留位置
工具开发特别容易陷入一个误区:
- 一开始想把界面做得特别完整
- 结果真正核心的硬件与测试流程反而推进很慢
所以我反而建议:前期 UI 保持克制,但预留扩展位。
这个项目接下来适合增加的 UI 区域
- 设备信息区:显示型号、容量、固件版本
- 测试项选择区:勾选要执行的用例
- 参数配置区:循环次数、LBA、块大小等
- 运行状态区:当前测试、进度、耗时
- 日志区:滚动显示实时日志
- 结果区:列表 + 详情面板
这样用户既能快速上手,也能承载越来越复杂的测试能力。
九、如果是我继续迭代,我会按什么优先级做?
如果你现在要把这套骨架继续往前推,我建议按下面顺序迭代:
第一优先级:接真实设备
先把 VendorUfsAdapter 打通。
因为没有真实硬件链路,很多上层能力的价值都还只是"逻辑正确"。
第二优先级:参数配置化
让测试不再依赖硬编码,能针对不同场景调整。
第三优先级:结果与日志落盘
保证每次执行都有迹可循。
第四优先级:进度与状态反馈
提升长时间测试的可用性。
第五优先级:测试编排与分组
让工具更接近正式的测试平台形态。
这个顺序的本质是:先打通真实能力,再提升可用性,最后增强平台化能力。
十、这套骨架适合哪些场景?
虽然项目名是 UFS,但我觉得这套结构并不只适合 UFS。
它同样适用于:
- eMMC 测试工具
- NVMe 测试工具
- 串口/USB/PCIe 设备验证工具
- 工厂产测工具
- 内部实验室自动化验证工具
因为它抓住的是一类通用问题:
- 底层设备接口变化大
- 测试项数量会持续增加
- 需要 UI 承载与结果展示
- 需要统一调度、取消、日志、报告
所以你可以把它理解为:
一个面向硬件测试工具的轻量工程骨架。
十一、结语
我一直觉得,测试工具真正的难点,不在于"写出第一个功能",而在于:
- 两个月后能不能继续加新测试
- 半年后能不能换设备实现
- 一年后能不能追溯某次失败现场
从这个角度看,一个好的骨架,比仓促堆出来的功能更重要。
当前这个 UFS 测试工具虽然还处在早期阶段,但它已经把后续最重要的扩展路径留出来了:
- 可以接真实 SDK
- 可以扩更多测试项
- 可以沉淀日志与报告
- 可以逐步走向平台化