
1. 项目概述一个被低估的工业级测试基建实践“Wally测试体系”这个名字听起来像某个开源玩具项目但实际它是一套在某嵌入式系统实验室里沉淀了五年、支撑过三款量产级边缘计算设备交付的测试基础设施。我第一次接触它是在帮某高校合作方排查一个固件升级失败的偶发问题时——他们用的不是Jenkins或GitLab CI那种通用流水线而是一套完全离线运行、所有测试用例能用命令行一键触发、C单元测试结果自动归档进结构化台账、连网络模块都物理断开也能跑通全链路验证的系统。标题里三个关键词“CLI黄金测试”“C测试台账”“无网络CI”不是修辞是硬性设计约束下的产物。它解决的不是“怎么写测试”的问题而是“在资源受限、环境不可信、交付周期压得极紧的工业现场如何让测试真正成为可信的质量守门员”。适合三类人细读一是正在为C项目搭建CI却卡在覆盖率统计和结果归档环节的工程师二是被客户要求提供可审计、可回溯、带时间戳与硬件指纹的测试报告的嵌入式团队三是需要在无外网、无云服务、甚至无局域网的封闭产线环境中部署自动化验证流程的技术负责人。它不追求炫技所有设计都指向一个目标当最后一块PCB板从贴片机下来测试报告PDF已经生成好U盘一插签字盖章发货。2. 整体架构设计与核心思路拆解2.1 为什么必须“无网络”——从交付场景倒推技术选型很多人第一反应是“没网络怎么拉镜像怎么传日志怎么连Git”这恰恰是Wally体系最根本的出发点。它服务的设备最终要部署在电力变电站、高铁信号箱、油田远程监测站等场景。这些地方要么物理隔绝光闸隔离要么网络策略极端严格只允许单向数据导出要么干脆没有IP网络仅RS485/Modbus通信。任何依赖外部服务的CI设计在这里都是纸面方案。所以Wally的第一条铁律是所有依赖必须本地化、所有通信必须进程内、所有状态必须可序列化为文件。它不反对网络而是把网络当作可选通道而非基础设施。比如日志上传不是“CI主动推送”而是“测试执行完后生成一个加密压缩包由运维人员手动拷贝到指定中转机”。这种“断网优先”思维直接决定了整个架构的底座——它不用Docker Swarm不用Kubernetes甚至不用systemd服务管理核心调度器就是一个用C17写的、静态链接的单二进制文件wally-runner启动时只读取本地config.yaml和testplan.json其余全部靠文件系统I/O驱动。2.2 “CLI黄金测试”不是指命令行好用而是指测试入口的绝对权威性与不可绕过性“黄金测试”这个词在Wally语境里有明确定义它是唯一被产品需求规格书PRD引用、被质量部门签字确认、且在出厂检验清单FAT中强制勾选的测试集合。它不等于全部测试而是最关键的37个用例——覆盖Bootloader校验、Flash擦写耐久性、RTC掉电保持、CAN总线错误帧注入响应等硬性指标。这些用例被固化在/opt/wally/golden/目录下每个用例是一个独立的.wtest描述文件YAML格式包含command调用哪个二进制、timeout毫秒级超时、expected_exit_code必须返回0、verify_script一个Python脚本用于解析串口输出或读取GPIO电平。关键在于wally-runner --golden这个命令是唯一被写入生产SOP的入口。产线工人不会、也不被允许去跑make test或ctest -R sensor_init。所有非黄金测试如压力测试、边界值扫描都放在/opt/wally/exploratory/下需加--force参数且记录操作日志。这种设计牺牲了灵活性换来了可审计性——质量部抽查时只要翻看/var/log/wally/runner.log就能看到每一台设备执行的黄金测试序列、时间戳、操作员ID绑定工控机USB键盘序列号、以及最终PASSED/FAILED状态。我亲眼见过某次客户Audit对方直接要求导出最近200台设备的黄金测试日志CSV整个过程不到90秒。2.3 C测试台账把GTest的原始输出变成可追溯、可聚合、可审计的工程资产市面上大多数C项目用Google Test跑完./test_binary --gtest_outputxml:report.xml就完了。Wally觉得这远远不够。它的“台账”Ledger概念源自会计学——每一笔测试执行都是一笔“交易”必须有凭证、有摘要、有借贷平衡即输入条件、预期输出、实测结果、差异分析。台账系统由三部分组成采集层wally-gtest-adapter一个轻量级包装器。它不修改原有GTest代码而是通过LD_PRELOAD劫持main()入口在RUN_ALL_TESTS()前后注入钩子捕获TEST_INFO、TEST_RESULT、ELAPSED_TIME并实时写入内存环形缓冲区。落盘层每执行完一个TEST_CASE立即序列化为一条Protobuf消息.proto定义见后文追加写入/run/wally/ledger.bin内存映射文件避免频繁磁盘IO。该文件按天轮转保留30天。台账服务wally-ledgerd一个常驻进程监听ledger.bin的inotify事件。一旦有新数据它解析Protobuf提取test_name、statusPASS/FAIL/SKIP、host_id设备唯一码、firmware_version、compiler_infoGCC 11.2.0 -O2 -mcpucortex-a72、hardware_signatureCPU ID RAM size Flash model然后插入SQLite数据库/var/lib/wally/ledger.db。这个DB就是真正的台账——它支持SQL查询“查所有sensor_driver_test在v2.3.1固件下失败的案例按hardware_signature分组统计次数”结果秒出。更关键的是台账导出功能wally-ledger export --formatpdf --date-range2024-01-01:2024-01-31会生成带数字签名的PDF每一页底部有SHA256哈希值确保报告不可篡改。这已不是测试报告而是具备法律效力的质量凭证。2.4 架构全景图没有中心节点只有文件与进程的契约Wally没有Master/Slave没有Server/Agent它的核心是四个进程两个文件系统位置的松耦合协作进程名启动时机核心职责通信方式wally-runner人工触发或定时器唤醒解析测试计划、派发任务、收集状态、写主日志读写/run/wally/ledger.bin、/var/log/wally/wally-gtest-adapter每个GTest二进制启动时自动加载拦截测试生命周期、序列化测试元数据LD_PRELOAD 内存映射ledger.binwally-ledgerd系统启动时由init.d拉起监听ledger.bin变更、解析写入SQLite、提供HTTP API仅限本地inotify SQLite3wally-reporterwally-runner执行完毕后调用从SQLite聚合数据、渲染PDF/CSV、生成数字签名SQLite读取 OpenSSL调用两个关键文件位置/run/wally/ledger.bin内存映射二进制流只追加永不删除是台账的“原始凭证”。/var/lib/wally/ledger.db结构化索引库是台账的“查询视图”。这种设计彻底规避了网络依赖——进程间通信全靠文件系统而Linux的ext4对小文件追加写性能极佳实测10万条记录写入200ms。当客户问“如果ledgerd崩溃了数据会不会丢”答案很直白“不会ledger.bin还在重启ledgerd会自动重放所有未处理记录”。这就是“无网络CI”的底气它不追求高可用而追求故障可恢复、状态可重建、过程可重演。3. 核心细节解析与实操要点3.1 CLI黄金测试的实现从YAML描述到原子化执行黄金测试的.wtest文件不是简单的shell脚本封装而是一套带约束的声明式协议。以bootloader_integrity.wtest为例# /opt/wally/golden/bootloader_integrity.wtest name: Bootloader CRC32 Check description: Verify bootloader image checksum matches embedded signature command: /usr/bin/check-bl-crc --image/dev/mtd0 --signature-offset0x1FFFC0 timeout_ms: 5000 expected_exit_code: 0 verify_script: /opt/wally/verify/bl_crc.py environment: PATH: /usr/local/bin:/usr/bin LD_LIBRARY_PATH: /opt/wally/lib required_hardware: - MTD_NAND - ARMv8 tags: [boot, security]关键细节解析command字段必须是绝对路径禁止./check-bl-crc或check-bl-crc强制要求which check-bl-crc的结果被写死。这是为了杜绝PATH污染导致的版本错乱。我们曾遇到一次事故产线机器PATH里混入了旧版工具链gcc命令指向GCC 4.9导致check-bl-crc链接了错误的libstdcCRC校验逻辑出错但返回码仍是0。Wally的解决方案是wally-runner在执行前会先readelf -d binary | grep NEEDED检查所有动态库路径若发现/usr/lib/libstdc.so.6系统默认以外的路径立即中止并报错[ERR] Binary links to non-system libstdc。verify_script是黄金测试的“第二道锁”command只保证进程退出码正确verify_script负责验证业务逻辑。bl_crc.py会解析check-bl-crc的标准输出JSON格式提取calculated_crc: 0x1a2b3c4d和embedded_crc: 0x1a2b3c4d做字符串比对。如果两者相等脚本exit 0否则exit 1wally-runner将本次测试标记为FAILED。这个脚本本身也受Wally管控——它必须位于/opt/wally/verify/下且其SHA256哈希值被硬编码在wally-runner的二进制里通过objcopy --add-section .verify_hashhash.bin注入每次执行前校验防止脚本被篡改。required_hardware是物理世界的守门员wally-runner启动时会读取/proc/cpuinfo、/proc/mtd、/sys/firmware/devicetree/base/compatible构建当前硬件指纹。若.wtest声明需要MTD_NAND但/proc/mtd里找不到mtd0:.*NAND则跳过此用例并记录[SKIP] Hardware not present: MTD_NAND。这避免了在x86开发机上误跑ARM专属测试导致的假失败。提示黄金测试的.wtest文件本身也是台账的一部分。wally-ledgerd会定期扫描/opt/wally/golden/将每个.wtest的name、sha256sum、mtime、tags写入SQLite的test_definition表。这样当某次Audit发现sensor_driver_test失败率突增你可以立刻查到“该用例定义文件在2024-01-15被更新过新版本增加了温度补偿逻辑而失败案例全部集中在v2.3.0固件未适配新逻辑”。3.2 C测试台账的Protobuf Schema为什么不用JSON或XML台账的原始数据流ledger.bin采用Protocol Buffers v3Schema定义如下ledger.protosyntax proto3; package wally.ledger; message TestRun { string test_name 1; // e.g., SensorDriverTest.InitSuccess string status 2; // PASS, FAIL, SKIP int64 start_time_us 3; // Unix timestamp, microsecond precision int64 end_time_us 4; int32 exit_code 5; // Process exit code string stdout 6; // First 1KB of stdout (truncated) string stderr 7; // First 1KB of stderr (truncated) string host_id 8; // Unique device ID (e.g., SN-ABCD1234) string firmware_version 9; // e.g., v2.3.1-rc2 string compiler_info 10; // e.g., GCC 11.2.0 -O2 -mcpucortex-a72 string hardware_signature 11; // e.g., cortex-a72-2G-MLC-NAND repeated string tags 12; // e.g., [driver, init] string gtest_xml_path 13; // Path to full GTest XML report (if generated) } message LedgerHeader { uint32 version 1; // Schema version (current: 1) bytes magic 2; // Fixed 8-byte magic: WALLYLED }选择Protobuf而非JSON/XML基于三点硬性考量体积与速度GTest单次运行产生约200个TestRun消息。实测10万条记录Protobuf二进制流大小为14.2MBJSON为48.7MBXML为62.1MB。更重要的是序列化耗时Protobuf平均38μs/条JSON为152μs/条。在资源受限的ARM Cortex-A7上这决定了ledger.bin能否做到毫秒级追加写而不阻塞测试主线程。向后兼容性version字段和magic头让ledgerd能安全处理未来升级的Schema。例如v2版本新增float temperature_celsius 14;字段老版ledgerdv1读取时会忽略该字段不影响解析新版ledgerdv2读取v1数据时temperature_celsius为0逻辑依然正确。JSON做不到这点——加个新字段老解析器可能直接panic。强类型与防错start_time_us必须是int64status只能是枚举值。这杜绝了JSON里常见的status: pass小写或start_time_us: 1700000000000000字符串这类低级错误。Wally的gtest-adapter在构造TestRun对象时若status不是预设值会直接abort()并打印[FATAL] Invalid status pass, expected PASS/FAIL/SKIP强制开发者修正。注意ledger.bin不是数据库而是日志。它不支持随机读取或删除。ledgerd的职责是“消费”它而不是“查询”它。所有复杂查询都发生在SQLite层。这种分离让写入极致轻量查询极致灵活。3.3 无网络CI的流水线编排用Shell脚本实现的“有限状态机”Wally的CI不是YAML配置而是一组精心编排的Shell脚本存于/opt/wally/ci/ci-prep.sh准备环境。挂载NFS共享如果可用、设置临时目录、校验/opt/wally/golden/完整性SHA256比对、备份旧台账。ci-build.sh编译。调用cmake -DCMAKE_BUILD_TYPERelWithDebInfo ... make -j$(nproc)但关键在make install后它会执行strip --strip-unneeded /opt/wally/bin/*并校验符号表大小防止调试信息泄露。ci-test.sh执行测试。核心逻辑是# Step 1: Run Golden Tests wally-runner --golden --log-levelERROR --output-dir/tmp/wally-report || { echo Golden tests failed; exit 1; } # Step 2: Run Coverage (only if requested network available) if [ -f /etc/wally/enable_coverage ]; then lcov --capture --directory . --output-file coverage_base.info # ... generate HTML report, but DO NOT upload it fi # Step 3: Generate Final Report wally-reporter --input-dir/tmp/wally-report --formatpdf --signed-byQA-Managerci-post.sh收尾。将PDF报告复制到/mnt/usb/report_$(date %Y%m%d_%H%M%S).pdf清空/tmp/wally-report发送系统通知wall CI completed. Report ready on USB.。这套脚本的精髓在于状态显式化。每个脚本开头都有# State file: /run/wally/ci.state # Valid states: PREP, BUILD, TEST, POST, DONE, ERROR STATE_FILE/run/wally/ci.state CURRENT_STATE$(cat $STATE_FILE 2/dev/null || echo PREP) if [ $CURRENT_STATE ! PREP ]; then echo [ERROR] CI in state $CURRENT_STATE, expected PREP 2 exit 1 fi echo BUILD $STATE_FILEci-build.sh执行完会把状态改为BUILDci-test.sh只在状态为BUILD时才运行。这种基于文件的状态机简单、可靠、无需额外依赖且cat /run/wally/ci.state就能一眼看出CI卡在哪一步。某次产线升级ci-build.sh因磁盘空间不足卡住运维人员SSH进去cat /run/wally/ci.state显示BUILDdf -h一看/tmp满了清理后echo BUILD /run/wally/ci.state再运行ci-test.sh5分钟内恢复。没有复杂的分布式协调只有清晰的文件状态。4. 实操过程与核心环节实现4.1 从零搭建Wally环境四步完成最小可行系统假设你有一台Ubuntu 22.04 ARM64开发机模拟目标设备环境以下是完整搭建流程。所有命令均经实测路径和权限严格匹配Wally规范。Step 1安装核心二进制与目录结构5分钟# 创建标准目录Wally强制约定 sudo mkdir -p /opt/wally/{bin,lib,golden,verify,ci} /var/lib/wally /var/log/wally /run/wally # 下载预编译二进制官方提供ARM64静态链接版 curl -L https://example.com/wally-bin-v1.2.0-arm64.tar.gz | sudo tar -xz -C /opt/wally/bin/ # 设置权限Wally要求所有二进制属主root不可写 sudo chown root:root /opt/wally/bin/* sudo chmod 755 /opt/wally/bin/* # 初始化台账文件空文件但需存在 sudo touch /run/wally/ledger.bin sudo chmod 644 /run/wally/ledger.bin sudo chown root:root /run/wally/ledger.bin # 初始化SQLite DB sudo sqlite3 /var/lib/wally/ledger.db /opt/wally/schema.sql # schema.sql内容见后文Step 2部署一个黄金测试用例3分钟创建/opt/wally/golden/hello_world.wtestname: Hello World Smoke Test description: Basic sanity check: print Hello Wally command: /bin/echo Hello Wally timeout_ms: 1000 expected_exit_code: 0 verify_script: /opt/wally/verify/hello_verify.py创建/opt/wally/verify/hello_verify.py#!/usr/bin/env python3 import sys import re # Read stdout from stdin (wally-runner pipes it) stdout sys.stdin.read().strip() if stdout Hello Wally: print(OK: Hello Wally printed correctly) sys.exit(0) else: print(fFAIL: Expected Hello Wally, got {stdout}) sys.exit(1)赋予执行权限sudo chmod x /opt/wally/verify/hello_verify.pyStep 3配置并启动台账服务2分钟创建/etc/wally/ledgerd.conf[database] path /var/lib/wally/ledger.db [ledger] bin_path /run/wally/ledger.bin watch_interval_ms 100 [logging] level INFO file /var/log/wally/ledgerd.log创建systemd服务/etc/systemd/system/wally-ledgerd.service[Unit] DescriptionWally Ledger Daemon Afterlocal-fs.target [Service] Typesimple Userroot ExecStart/opt/wally/bin/wally-ledgerd --config/etc/wally/ledgerd.conf Restartalways RestartSec10 [Install] WantedBymulti-user.target启用并启动sudo systemctl daemon-reload sudo systemctl enable wally-ledgerd sudo systemctl start wally-ledgerdStep 4执行首次黄金测试并验证台账2分钟# 执行黄金测试 sudo /opt/wally/bin/wally-runner --golden --log-levelINFO # 查看主日志 sudo tail -n 20 /var/log/wally/runner.log # 应看到[INFO] Running golden test: Hello World Smoke Test ... [PASS] # 查询SQLite台账应有1条记录 sudo sqlite3 /var/lib/wally/ledger.db SELECT test_name, status, host_id FROM test_run ORDER BY id DESC LIMIT 1; # 输出Hello World Smoke Test|PASS|ubuntu-arm64-20240101至此一个最小Wally系统已运行。整个过程不涉及任何网络下载二进制已预置、不依赖外部服务、所有路径和权限符合规范。你可以立刻把它烧录到SD卡插到目标ARM设备上运行。4.2 台账数据库schema详解为审计而生的设计/opt/wally/schema.sql是台账的基石其设计直指审计需求-- 主表test_run每行对应一次测试执行 CREATE TABLE IF NOT EXISTS test_run ( id INTEGER PRIMARY KEY AUTOINCREMENT, test_name TEXT NOT NULL, status TEXT NOT NULL CHECK(status IN (PASS,FAIL,SKIP)), start_time_us INTEGER NOT NULL, end_time_us INTEGER NOT NULL, exit_code INTEGER, stdout TEXT, stderr TEXT, host_id TEXT NOT NULL, firmware_version TEXT, compiler_info TEXT, hardware_signature TEXT NOT NULL, tags TEXT, -- JSON array string, e.g., [smoke,hello] gtest_xml_path TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- 索引加速按设备、按固件、按时间查询 CREATE INDEX IF NOT EXISTS idx_host_firmware ON test_run(host_id, firmware_version); CREATE INDEX IF NOT EXISTS idx_time ON test_run(start_time_us); CREATE INDEX IF NOT EXISTS idx_status ON test_run(status); -- 定义表存储.wtest文件元数据支持追溯测试定义变更 CREATE TABLE IF NOT EXISTS test_definition ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE, sha256_hash TEXT NOT NULL, mtime INTEGER NOT NULL, -- Unix timestamp tags TEXT, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- 报告表记录每次wally-reporter生成的报告 CREATE TABLE IF NOT EXISTS report_generation ( id INTEGER PRIMARY KEY AUTOINCREMENT, format TEXT NOT NULL CHECK(format IN (pdf,csv)), output_path TEXT NOT NULL, signed_by TEXT, generated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, ledger_start_id INTEGER, -- 外键关联test_run.id ledger_end_id INTEGER -- 外键关联test_run.id ); -- 触发器当插入test_run时自动更新test_definition.last_used CREATE TRIGGER IF NOT EXISTS update_def_last_used AFTER INSERT ON test_run BEGIN INSERT OR REPLACE INTO test_definition (name, sha256_hash, mtime, tags) VALUES (NEW.test_name, (SELECT sha256_hash FROM test_definition WHERE name NEW.test_name), (SELECT mtime FROM test_definition WHERE name NEW.test_name), (SELECT tags FROM test_definition WHERE name NEW.test_name)); END;这个schema的审计友好性体现在test_run.host_idtest_run.firmware_version是天然的复合主键确保同一设备同一固件版本的重复测试不会被误认为新数据。report_generation表的ledger_start_id/ledger_end_id记录了该PDF报告覆盖的测试记录范围。审计时只需SELECT * FROM test_run WHERE id BETWEEN X AND Y就能100%还原报告生成时的原始数据杜绝“报告是A但后台数据已变成B”的风险。test_definition表存储了所有黄金测试的定义快照。即使你删掉了/opt/wally/golden/hello_world.wtesttest_definition里仍保留着它被最后使用时的SHA256和时间戳证明“这个测试在2024-01-01确实存在且被运行过”。实操心得SQLite的WALWrite-Ahead Logging模式是Wally台账的性能关键。我们在/var/lib/wally/ledger.db上执行sqlite3 /var/lib/wally/ledger.db PRAGMA journal_modeWAL;。这使得ledgerd的写入操作几乎不阻塞wally-runner的读取ledger.bin解析是读密集ledger.db写入是写密集实测并发100个测试进程时ledger.db写入延迟稳定在5ms。4.3 无网络CI的“离线发布”工作流从产线到客户签收Wally的CI终点不是“Merge to main”而是“U盘弹出”。一个典型的离线发布工作流如下阶段1产线预检Pre-Shipment Check工控机Ubuntu ARM64连接待检设备通过USB-TTL串口。运维人员双击桌面图标Run Final FAT触发/opt/wally/ci/ci-prep.sh /opt/wally/ci/ci-test.sh。ci-test.sh执行全部37个黄金测试耗时约4分30秒含Bootloader擦写等待。测试完成后wally-reporter生成/mnt/usb/FAT_Report_SN-ABCD1234_20240101_143022.pdf文件名含设备SN和时间戳。PDF内容包括封面设备SN、固件版本、测试日期、黄金测试汇总表37行每行Test Name | Status | Duration | Notes、详细日志节选每个FAIL用例的stderr、数字签名区块含公钥指纹和SHA256哈希。阶段2客户签收Customer Acceptance客户收到设备和U盘。客户在自有Windows PC上打开PDF点击“属性”-“数字签名”-“详细信息”验证签名有效性证书由某高校CA签发公钥已预置在客户系统。客户核对PDF中的hardware_signature如cortex-a72-2G-MLC-NAND与设备实物标签一致。客户在PDF末页手写“验收合格”并签字扫描存档。阶段3内部追溯Internal Traceability若后续设备出现故障质量部输入SNABCD1234执行SELECT * FROM test_run WHERE host_idSN-ABCD1234 ORDER BY start_time_us DESC LIMIT 5;结果立即显示该设备出厂时所有测试详情包括compiler_info确认是否用了正确的工具链、gtest_xml_path可定位到完整的XML报告文件用于深度分析失败原因。这个工作流的核心价值是所有证据链设备、固件、测试、报告、签名物理隔离、时间锁定、不可抵赖。它不依赖任何在线服务不产生任何云端日志所有数据都在U盘和设备本地。某次客户质疑“你们的测试是不是造假”我们当场拿出U盘用另一台电脑打开PDF现场验证数字签名并展示/var/lib/wally/ledger.db里对应的原始记录。客户沉默三秒说“这比我们的ERP系统还严谨。”5. 常见问题与排查技巧实录5.1 黄金测试执行失败90%的问题出在这里现象根本原因排查命令解决方案wally-runner --golden报错[ERR] Binary links to non-system libstdc测试二进制链接了自建GCC的libstdc而非系统/usr/lib/libstdc.so.6readelf -d /opt/wally/bin/test_binary | grep NEEDED重新编译测试二进制添加-static-libstdc或用patchelf --set-rpath /usr/lib /opt/wally/bin/test_binary黄金测试显示[PASS]但verify_script输出FAILverify_script未被正确执行或wally-runner未捕获其退出码sudo strace -e traceexecve,wait4 -p $(pgrep wally-runner) 21 | grep verify检查/opt/wally/verify/下脚本权限是否为755且首行#!/usr/bin/env python3指向正确解释器确认wally-runner版本≥1.1.0旧版有verify脚本调用bugci-test.sh卡住/run/wally/ci.state显示TESTwally-runner进程僵死通常因timeout_ms设置过短测试未完成就被SIGKILLsudo ps aux | grep wally-runnersudo cat /proc/$(pgrep wally-runner)/stack增加.wtest中timeout_ms值检查测试二进制是否进入死循环如等待不存在的硬件中断用wally-runner --debug运行单个测试观察实时输出wally-ledgerd日志报错SQLITE_BUSY: database is locked多个进程同时写ledger.db但Wally设计为单写仅ledgerd此错误表明有外部程序如sqlite3命令行在直接操作DBsudo lsof /var/lib/wally/ledger.db杀死所有非wally-ledgerd的持有者确保/var/lib/wally/目录权限为755 root:root禁止其他用户写入踩过的坑某次产线升级ci-test.sh在wally-runner后加了 sync命令意图确保日志落盘。结果sync耗时2秒导致ci-post.sh启动时/run/wally/ci.state仍是TESTci-post.sh因状态校验失败而退出。最终方案是移除sync信任Linux的fsync()在ledgerd写入SQLite时已足够若真需强制同步用sudo fdatasync /var/lib/wally/ledger.db毫秒级。5.2 台账数据丢失先别慌原始凭证还在ledger.bin当wally-ledgerd崩溃或ledger.db损坏第一反应不是重装而是抢救ledger.bin。Wally提供了wally-recover工具# 步骤1停止ledgerd sudo systemctl stop wally-ledgerd # 步骤2从ledger.bin重建ledger.db会清空现有DB sudo /opt/wally/bin/wally-recover --input/run/wally/ledger.bin --output/var/lib/wally/ledger.db # 步骤3验证重建结果 sudo sqlite3 /var/lib/wally/ledger.db SELECT COUNT(*) FROM test_run; # 应输出与之前相近的数字如12487 # 步骤4重启ledgerd sudo systemctl start wally-ledgerdwally-recover的原理是逐字节扫描ledger.bin识别WALLYLED魔数解析每个TestRunProtobuf消息跳过损坏的记录日