Skip to content

Latest commit

 

History

History
357 lines (276 loc) · 23.8 KB

testing-guide.md

File metadata and controls

357 lines (276 loc) · 23.8 KB

Engine 测试使用指南

本指南面向第一次在 Engine 仓库新增或修改测试的贡献者。长期边界见 Engine 测试架构,当前采用清单见 测试采用清单。

1. 先选择测试层级

需要证明什么 首选层级 是否使用 testkit
单个纯函数、值对象、状态转换 Core/Application 普通单元测试 否。使用 just cargo test
一个 Application 完整负责人,使用内存 port、可控消息或可控时间 Application 场景测试 仅在需要阶段、等待、资源或结构化失败证据时使用
SQLite、文件、密码、codec、provider 合同 Infra integration/provider 有多阶段、资源或失败分类时使用
稳定 uc-engine 公开入口的短链路 Engine smoke/contract 有跨阶段诊断需要时使用
独立进程、崩溃、重启、持久恢复 process 使用,统一记录退出、预算与清理
network namespace、relay、真实发现与断线 real-network 保留专用脚本;testkit 不能替代真实链路
模拟器、实体设备、绑定和宿主 device 保留平台宿主;未运行只能记为跳过

简单、快速、没有资源生命周期的单元测试不强制套 Scenario。不要为了统一外观把纯规则测试改写成场景 DSL。

2. 复用现有支撑

新增 fixture 前按顺序检查:

  1. 目标业务目录现有 tests 或 test_support,优先调用真实完整负责人。
  2. crates/uc-application/src/test_support/ 中跨业务测试已经共用的窄支撑。
  3. uc-testkit 的场景身份、预算、事件等待、临时目录、端口和子进程能力。
  4. .config/nextest.toml 已有分组是否能准确选中测试。

testkit 不解释 admission、membership、provider 或存储状态,也不复制生产状态机。只有多个真实调用方已经需要相同的测试生命周期能力时,才向 testkit 增加入口。

文件放在哪里

  1. 场景只需要 crate 公开接口:放在该 crate 的 tests/。
  2. 场景必须读取私有状态或调用私有测试装配:放在业务模块的 src/**/tests/,由模块的 #[cfg(test)] 入口接入。
  3. 只服务该领域场景的 fixture:放在相邻 tests/support/;已有明确领域测试网络可放在 testing/。
  4. 跨领域通用能力才进入 tests/uc-testkit/。

不要为了把测试移到 crate tests/ 而把生产私有接口改为 pub,也不要新增生产测试开关。短小的既有 tests.rs 和明确命名的历史 test_support.rs 可以保留;新增长场景不能继续内嵌在业务实现文件中。

3. 可直接运行的最小示例

从仓库根目录执行:

just cargo test -p uc-testkit --test scenario_demo --locked

运行成功和受控失败示范,并把两次工件写到同一根目录:

export UC_TEST_ARTIFACTS_DIR=target/test-artifacts/guide
just cargo run --quiet --locked -p uc-testkit --example scenario_demo -- success
just cargo run --quiet --locked -p uc-testkit --example scenario_demo -- failure
find "$UC_TEST_ARTIFACTS_DIR" -name result.json -o -name summary.txt

受控失败示范本身以成功退出,因为它先验证失败分类和工件;result.json 的 outcome 仍为 failed。每次运行会创建独立的安全实例目录,相同场景并行或重复运行不会覆盖。

4. 场景最小结构

use std::time::Duration;

use uc_testkit::{Scenario, ScenarioBudget, ScenarioConfig};

async fn run_scenario() {
    let scenario = Scenario::start(ScenarioConfig::new(
        "provider-contract",
        0x504f_5254,
        ScenarioBudget::new(Duration::from_secs(2)),
        "cargo nextest run -p my-crate -E 'test(provider_contract)'",
        "target/test-artifacts/provider-contract",
    ))
    .expect("scenario starts");

    scenario.record_event("request-sent");
    scenario.record_event("response-accepted");
    scenario.finish(Ok(())).expect("scenario report");
}

场景名、阶段名、事件、资源 kind/label 和 condition 必须是静态、脱敏的稳定类别,不能包含设备名、地址、邀请、令牌、文件名、绝对路径或业务正文。

5. 预算、等待和资源

  • ScenarioBudget 是整个场景的 wall-clock 保护预算,不是产品业务 deadline。
  • stage("stable-name") 只记录阶段耗时;guard 离开作用域时结束。
  • wait_for_event 使用事件通知,不用固定 sleep 轮询产品状态;失败包含 condition 与最后事件。
  • temp_dir 创建权限受控并在 Drop 时清理的目录。
  • tcp_port 持有已绑定 listener,避免“探测后释放”的端口竞争。
  • run_child_process 在显式预算内 wait;超时后 kill 并再次 wait,资源 cleanup 写入报告。非零退出码由业务场景决定属于产品失败还是预期故障注入。
  • 外部资源只有调用方确实拥有其生命周期时才用 record_external_resource 登记结果。

涉及等待者退出的用例,先通过既有完整查询或受控能力的开始屏障确认业务动作已进入负责人,再触发等待超时或取消。 固定睡眠、future创建或等待期限到达本身不能证明动作已开始;调度延后可能让Engine在调用业务入口前就完成取消。 只在确认开始后验证已启动工作继续受监督,并保持原清理、资源及期限断言;不要为测试扩大稳定Engine的业务步骤接口。

nextest 仍是测试进程的最终超时负责人。场景预算应短于 nextest override,给 JSON/摘要写入和清理留出时间。

进度连接的真实空闲回收用例 actual_work_leases_survive_thirty_seconds_then_real_idle_timer_closes_the_connection 必须先保留实际工作31秒,再等待原30秒空闲回收;默认20秒终止线无法完成该证明。 .config/nextest.toml 为该精确用例在 default/ci 配置120秒外层期限,并归入 real-network 组。 测试自身90秒截止、产品30秒策略、实际等待断言和零重试均保持不变;不能把这个外层预算泛化到其他用例。

独立进程的startup_overlapping_a_closing_host_is_never_reported_as_corrupt连续12轮执行创建、关闭和重启, 覆盖六种重叠延迟。固定原二进制单CPU直接执行26.42秒完成,而原nextest在20.006秒终止; 因此该精确用例在default/ci使用120秒外层预算以覆盖完整流程及coverage开销。 单次宿主应答30秒、原操作期限、12轮、损坏分类断言及零重试保持,不扩大同文件其他测试的预算。 这是外层测试预算修正;CI同名超时仍须由候选CI验证,不据此推断所有启动失败的原因。

6. 分组与本地运行

统一入口:

bash scripts/testing/run-test-group.sh fast
bash scripts/testing/run-test-group.sh evidence
bash scripts/testing/run-test-group.sh persistence-provider
bash scripts/testing/run-test-group.sh engine-smoke
bash scripts/testing/run-test-group.sh process
bash scripts/testing/run-test-group.sh membership-e2e
bash scripts/testing/run-test-group.sh upgrade-matrix --smoke

membership-e2e 运行 crates/uc-engine/tests/space_membership_auto_pairing_e2e.rs:每个场景启动 2–10 个完整 Engine,使用真实 SQLite、MLS 与本机回环 Iroh QUIC,配对 rendezvous 为本地模拟服务,分区由 dev-tools 在 Engine 内施加,使用真实时钟。该文件只在 dev-tools feature 下编译,因此 cargo test -p uc-engine --locked 不运行 其中任何场景。nextest 为每个场景单独起进程,并按 .config/nextest.toml 的 membership-e2e 测试组限制并发、 放宽慢测试期限;cargo test 会在一个进程中顺序运行全部场景,耗时约为前者的数倍且时序失真,只作为兼容入口保留。 RejectNewConnections 的协议选择必须覆盖对应能力的全部当前版本;剪贴板协议新增能力版本时, 同时更新 dev-tools 的内容拨号选择。真实故障测试必须核对实际拒绝计数,不能只启用开关就假定 目标交换已经受影响;已有连接的复用与新拨号拒绝分别验收。

组更新 G4 验证真实多节点的收件人隔离、持续故障时队首不变与解除故障、真实重新认证后的连续 epoch 恢复。 真实网络在观察期间仍可能产生新认证证据,不能把固定睡眠视为“无证据窗口”。退避次数合同由 space_security_store::tests::unavailable_recipient_has_at_most_one_due_retry_in_a_quiet_25_second_window 在真实 SQLite、明确时钟与重开仓储后严格验证;Application 的 each_fresh_online_evidence_allows_one_early_retry_but_quiet_rounds_do_not 验证证据消费与安静轮次。 这两项属于 workspace 门禁,G4 仍属于 membership-e2e 门禁。G1 继续覆盖不重启的自动恢复;G4 的接收方 重启提供真实新认证证据,不清除发送队列。解除接收方本机故障不会自动通知发送方,不能假定任意次数失败后的 下一次退避仍小于测试固定恢复预算。

场景按类别放在 crates/uc-engine/tests/space_membership_auto_pairing_e2e/ 的模块中(admission、topology、 removal_convergence、space_switch、membership_history、automatic_connections 等),共享测试台位于其 harness/。topology:: 下的多节点拓扑另属 membership-topology 测试组,并发更低。每个场景第一行是 let _scenario = TestScenario::start();:测试台经它登记设备目录、拓扑动作事件与等待阶段,守卫释放时(含 panic) 在 $UC_TEST_ARTIFACTS_DIR/membership-e2e/ 下写出 result.json 与 summary.txt;测试台等待超时记为 product_timeout 并带条件名,其余断言失败记为 product_invariant。新增场景必须保留这一行,且只描述设备动作与 业务断言,不自行管理目录、超时或工件。可追加 nextest 参数选择场景,例如:

bash scripts/testing/run-test-group.sh membership-e2e -E 'test(f7_)'
RUST_LOG=warn,uc_application::space::membership=debug \
  bash scripts/testing/run-test-group.sh membership-e2e -E 'test(removal_convergence)' --no-capture

upgrade-matrix 运行升级兼容矩阵(计划 051): tests/upgrade-matrix/anchors.json 列出各 Desktop 公开发布锁定的 Engine rev。矩阵只覆盖当前源码:每个锚点到当前 源码的四个维度(D3 分两种邀请方向),加一条单设备经过全部可运行锚点到当前源码的完整链,锚点数为 n 时共 5n + 1 个 单元;已发布锚点之间的组合不随当前源码变化,不纳入矩阵。expectations.json 登记每个单元的 期望(pass、rejected、known-incompatible 或 skip,后三者必须写明原因与链接)。rejected 用于配对本应 失败的单元:登记阶段与加入方公开拒绝原因(rejection_reason),只有失败条件为 join-rejected 且原因一致才算相符; known-incompatible 与 skip 另须登记失败条件。build.rs 把清单展开为具名测试, 例如 d1::a13_to_head、d3::a13_to_head_old_inviter、d1::chain;每个单元一个 uc_testkit::Scenario,经 tests/hosts/connectivity 宿主驱动各版本的公开 Engine 操作,使用本地 rendezvous 与本机回环直连,不访问外部服务。 入口先用 scripts/testing/build-upgrade-anchors.sh 按 rev 构建并缓存旧版宿主(<target>/upgrade-anchors/<rev>/bin, 宿主源码或补丁变化时自动重建,--clean 回收),再交给 nextest 的 upgrade-matrix 测试组。--smoke 只跑上一个 锚点到当前源码的 5 个单元,--dimension d1|d2|d3|d4 只跑一个维度,其余参数交给 nextest。CI 在 PR 上运行冒烟, 在夜间、锚点定义(anchors.json、host-features.json、anchors/)变化的 PR 与手动触发时运行整个矩阵:

bash scripts/testing/run-test-group.sh upgrade-matrix --smoke
bash scripts/testing/run-test-group.sh upgrade-matrix
bash scripts/testing/run-test-group.sh upgrade-matrix --dimension d4
bash scripts/testing/run-test-group.sh upgrade-matrix -E 'test(=d1::a05_to_head)'
bash scripts/testing/build-upgrade-anchors.sh --clean

A03、A04 在仅局域网模式下只经 mDNS 解析邀请码并发现对端,因此矩阵要求本机允许回环组播;在屏蔽组播的命令沙箱中 运行时,这两个锚点的配对单元会以 join(错误码 1234)或 peer-not-paired 失败,属于环境问题,不能作为产品结论。 由这两个锚点完成配对的 D2/D3 单元在 nextest 中独占运行,避免并发负载使邀请方广播错过加入方的 5 秒窗口。

发送前测试台等待双方公开快照稳定(对端关系、进行中的成员变更、加入状态),只发送一次并以接收方收到为准; 加入方公开加入状态为拒绝或终止时立即记为 join-rejected 并保留原因。

每个单元目录除 result.json、summary.txt 外还有 cell.json(登记、实际结果、宿主身份与能力、脱敏事实); 失败或跳过时附宿主 stderr 与 Engine 日志尾部。入口结束时汇总为同目录的 matrix.md 与 matrix.json。实际结果与 登记不一致(包括登记为已知不兼容却通过、未登记的跳过)时测试失败;不得通过修改登记掩盖产品失败。旧版公开接口的 差异由宿主按能力划分的 feature 表达,锚点与能力的对应见 tests/upgrade-matrix/host-features.json;只有能力分层 无法覆盖时才为锚点新增 tests/upgrade-matrix/anchors/<锚点>.patch,且只能改宿主与工作区成员声明。 automatic-sync-control 表达 Engine 1.0.0-rc.8(ef3ccc2d)起的独立自动同步开关,a03 起启用; a01/rc.1 与 a02/rc.6 不支持时必须明确拒绝 auto_sync_enabled,不能映射到语义不同的旧 auto_sync。 sync_on_restore 保持独立可用,缺少本地网络配置能力的旧宿主仍在启动时返回 local_network_unavailable。

real-network 会转交现有 Linux 网络脚本;device 要求明确平台与设备,不会自动运行。cargo-nextest 必须为脚本声明的固定版本;脚本不会静默退回语义不同的 runner。

新增测试接入分组时:

  1. 优先按 package、test binary 或稳定测试模块名写 nextest filter。
  2. 为共享端口、数据库或全局 subscriber 等资源设置串行 test-group。
  3. 给测试设置比场景预算更长的 slow-timeout。
  4. 先保留原 cargo test 入口并双轨运行,不直接删除旧门禁。
  5. 在 PR workflow 的 evidence 入口增加测试时,同步上传其 JSON/摘要目录。

双1GiB资源基线仅在开发者本机运行

managed_engine_two_one_gib_files_stream_with_bounded_memory_and_settle_fd_and_tasks 标记为 #[ignore]。按用户确认的验收范围,工作区、覆盖率和默认CI生命周期分组都不运行该用例; CI仍强制执行其余8项生命周期、下载配额与容量场景,报告中大文件项明确记为 skipped,不读取旧的大文件报告。

在支持 /proc 资源采样的Linux开发机上显式运行完整资源验收:

bash scripts/testing/run-blob-lifecycle-resources.sh --include-local-large

此入口额外运行原双1GiB用例并严格校验其报告;CI环境拒绝该选项。原两轮真实传输、逐块内容读回、 SDK tag/原生连接、FD/线程/任务和内存阈值均保留。默认不传选项仍是CI的8项入口。 主线覆盖率运行曾在第二轮任务数增长断言失败,按原记录保留;移出CI只调整验收场所,不把该失败改记为通过。

7. 查看和复现失败

nextest 的 JUnit 位于:

target/nextest/ci/junit.xml

场景目录包含 result.json 和 summary.txt。先读摘要,再核对 JSON 的:

  • failure.kind 与 failure.secondary
  • failure.condition
  • last_event
  • stages
  • resources 与 cleanup
  • reproduce
  • artifact_directory

运行报告中的 reproduce 命令,不要根据失败文本手工猜过滤条件。若测试进程在 Scenario finish 前 panic/abort,场景 JSON 可能不存在;此时以 nextest/JUnit 和进程输出为准。本仓不安装全局 panic hook 改变其他测试行为。

8. CI 接入检查表

  • 旧 cargo test 或专用脚本仍可运行。
  • 新测试进入正确分组,没有被 fast 意外选入真实网络或设备集合。
  • CI 上传 JUnit、JSON 和文本摘要,if: always() 保留故障证据。
  • CI 各组运行前清空共用的 target/nextest/ci/junit.xml,组名报告只从对应步骤的 success/failure 结果保留。 取消或跳过不能把前一组留下的 JUnit 复制成当前组报告;原始文件可随工件保留,但必须结合实际步骤、用例身份与来源核对,不能仅看文件名或零失败计数。
  • workspace 测试失败时先上传该组 JUnit 为 engine-workspace-failure,便于在后续长测试结束前定位;整组结束后的完整工件仍按原入口保留。编译未产生报告时不伪造空报告,取消/跳过不触发此失败工件。
  • 默认不重试;任何 retry-pass 必须单独统计,不能当作稳定通过。
  • 真实网络和设备未执行时明确写“跳过/未验证”。
  • 先读取实际工件再报告成功,不以 workflow 绿色代替报告验收。

9. 快速线与真实环境线

  • 日常和 PR 使用快速确定性线。单场景尽量控制在 1 秒内,整组控制在 1 分钟内,均不含编译。
  • nightly 使用真实 Engine 多进程、独立身份/资料/端口、真实存储和对应的真实网络环境;准备、测试和清理合计 目标为 30 分钟内。编译耗时和整次总耗时必须单独展示。
  • 真实环境 workflow 应允许手工选择单个场景。网络故障必须由 network namespace 或对应真实环境实际施加,不能 用 mock 返回错误替代。
  • 超时是失败,不通过扩大预算、放宽断言、删除覆盖或自动重试隐藏。确需重跑时保留首次失败工件,并把 retry-pass 单独登记。
  • 发布前检查近期 nightly 结果,并补跑受变更影响的配对、文字/文件传输、断线重连、重启恢复或旧资料升级场景。

当前不要把两条线混用:VirtualMembershipNetwork 只是配对后成员历史的快速 fixture;快速文字场景证明 ClipboardSyncFacade 的 V3 编码与 accepted fan-out,快速文件场景只证明公开 FileTransferFacade 的完成生命周期; 完整配对和 E02 文字/文件 传输属于真实 Engine runner。五类逐项状态和实测值以测试架构的首批业务覆盖矩阵 为准。

快速 admission 场景的作者入口保持窄小:准备 JoinSpaceInput,调用一次 complete_joiner_pairing,只断言返回的 Active 状态和最终确认。fixture 内部调用真实 SpaceAdmissionProtocol、成员维护与激活负责人;测试不得编排 Candidate/Commit/Complete/ACK 或固定恢复轮次。当前示范可直接运行:

just cargo nextest run -p uc-application \
  -E 'test(joiner_pairing_fixture_reaches_active_settled)' --locked

这个快速入口只证明加入方确定性规则。邀请方最终确认唯一性由三设备场景证明;双方 Engine 的 same-space、usable、 online 和真实传输仍必须运行下面的真实 runner。

两节点成员历史的作者示例位于 crates/uc-application/src/space/membership/testing/virtual_membership_network.rs。场景正文只准备两个节点并声明操作和预期:

let nodes = TwoMemberHistoryScenario::prepare(&scenario, 3)?;
expect_confirmed(nodes.exchange().await?);
nodes.partition()?;
expect_unavailable(nodes.exchange().await);
nodes.heal()?;
expect_confirmed(nodes.exchange().await?);

TwoMemberHistoryScenario 内部负责真实 Application endpoint、ledger、节点身份、注册、frame 预算、故障生命周期, 并把节点准备、分区、恢复和 exchange 结果写入通用 Scenario;后者负责时间预算、阶段、清理和报告。单独运行:

just cargo nextest run --profile ci --locked -p uc-application \
  -E 'test(two_member_nodes_partition_and_heal)'

该入口是成员历史领域 fixture,不是跨业务通用多节点 DSL,也不证明真实 Engine transport 重连。

真实 runner 的最小调用示例:

# 完整配对
bash scripts/testing/run-connection-recovery-e2e.sh --suite network --mode direct --repeat 1 --case E01

# 只登记文件传输场景;runner 自动完成未登记的必要配对/文字 setup
bash scripts/testing/run-connection-recovery-e2e.sh --suite network --mode direct --repeat 1 --case E02-file

这两个命令需要 Linux network namespace 和相应权限。macOS 上的脚本语法或 host 编译通过不构成场景通过。 runner 工件中的 timings.prepare_ms、scenario_ms、cleanup_ms 和 total_ms 用于核对 30 分钟目标;场景 records 仍保留每个业务步骤耗时,二者不能互相替代。

当前 draft PR 的等价隔离 Linux job 已使用 --repeat 3 逐 mode 实测:

mode prepare scenario cleanup total
direct 2.938 秒 652.479 秒 1.391 秒 656.807 秒
known-peer 22.280 秒 62.929 秒 0.539 秒 85.748 秒
relay 5.735 秒 131.415 秒 0.514 秒 137.663 秒
legacy 3.544 秒 0.504 秒 0.625 秒 4.674 秒

四个工件均记录 failed=false、cleaned=true 和 plaintext_clean=true,因此当前 runner 的单 mode 准备、场景和清理满足 30 分钟目标。新增 workflow 尚未进入默认分支,scheduled 与 profile-upgrade 的首次远程样本 仍须在合并后取得;这不影响上述 runner 实测,也不能把它写成 nightly 已持续稳定。

编译缓存入口

测试分组与连接恢复脚本自动使用仓库固定的 MBX。直接 Cargo 命令用 just cargo …,或先 source scripts/build-cache/env.sh;不要从绝对路径绕过仓库入口。缓存只恢复编译产物,测试仍每次实际运行并生成自己的工件。统一缓存与 CI/发布信任边界见 ADR-033。

PR 检查范围

PR Check 与 Windows Build 共用 范围分类器,只对 PR 优化: 纯 docs/ Markdown(不含 generated、AGENTS 及被 Rust 测试嵌入的 observability.md)或根 README 运行分类回归与补丁空白检查; 精确的 mbx-snapshot.py、mbx-snapshot.test.py 变更(可附带上述文档)运行 macOS、Linux、Windows x64/arm64 快照 SDK 测试与真实 cold/export/import/hit/失效回归, 不构建无关产品矩阵。Linux 结果由保留原名的 Engine repository checks 汇总,失败或意外跳过不能通过。 MBX 专项仍保留既有平台、凭据边界与实际 Engine 迁移/缓存消费者验证。

分类读取 PR base 的脚本,以完整 Git merge-base diff 的原始记录判定;重命名拆为删除和新增, 两端都必须满足白名单。未知文件、其他脚本、产品代码混合、文件类型/执行位改变、分类器或工作流 自身变更、空 diff、历史或分类失败均执行完整产品检查。首次引入分类器的 PR 同样执行完整检查。 主线 push、merge queue、手动运行和 full-ci 标签不缩减验证;添加 full-ci 会重新运行产品门禁。 未改变 workflow 的 PR 路径触发条件,范围外 job 以显式 skipped 结束,不冒充产品测试已运行。 现有检查名称保留;远端必需检查配置由仓库维护者管理,不能从 workflow 文件推断。

快照测试 action 独立安装固定 Boto3,显式设置 测试的 PYTHONPATH 并检查 SDK 导入;安装失败会阻断测试,不会退回 SDK 缺失时的 unittest skip。 快照传输 action 的可选 SDK 安装路径不能代替测试环境装配。分类回归入口为 python3 scripts/ci/pr-scope.test.py。