框架测试与基准
本页面只服务于 Wow 框架仓库:修改框架源码、TCK、Adapter、构建逻辑或基准时,用这里的任务收集证据。业务应用发布请使用Wow 应用测试,不要复制本仓库的根任务、Codecov flag 或 JMH 结论。
完成信号
框架变更完成时,受影响模块的 check 以及对应测试层已通过;若涉及基准入口,smoke 已通过;若声称性能变化,还要有同一工作负载、可比环境和完整 provenance 的基线/确认结果。下一层是代码审查与 CI,不是把历史数字改写成产品承诺。
先按依赖选测试层
| 层 | Source set | 根任务 | 运行条件 | 证明范围 |
|---|---|---|---|---|
| Local | src/test | allLocalTest | 不需要容器 | 本地安全的单元、领域和组件行为 |
| Contract | src/contractTest | allContractTest | 不需要容器 | 已注册 TCK 实现满足共享契约 |
| Integration | src/integrationTest | allIntegrationTest | 需要 Docker/Testcontainers | 中间件 Adapter 与端到端集成 |
根构建当前只为 :wow-core、:wow-opentelemetry 和 :wow-mock 注册 contractTest;只为 :wow-bi、:wow-mongo、:wow-redis、:wow-kafka、:wow-elasticsearch 和 :wow-it 注册 integrationTest。不要为不存在的模块猜任务名。
check 运行标准 test,并在已配置的模块中包含 contractTest;它不会自动运行容器型 integrationTest。因此“check 绿色”不能推导出所有存储与 Broker 集成均已验证。
最窄的本地反馈
先运行直接受影响模块:
./gradlew :wow-core:check
./gradlew :wow-test:check :example-domain:check需要整个本地安全层时再扩大:
./gradlew allLocalTest
./gradlew allContractTest
./gradlew check领域规格仍使用领域测试套件中的 AggregateSpec 和 SagaSpec。它们位于所属模块的 src/test,属于 Local 层,不是单独的应用发布证明。
架构护栏
两类测试守护模块边界与装饰器,都属于 Local 层:
DependencyRulesTest(:wow-core:test)扫描生产源码。wow-core与wow-query不引用 HTTP、Spring 或存储驱动类型。后端模块(wow-mongo、wow-redis、wow-kafka、wow-elasticsearch)互不引用,也不引用其他后端的驱动。wow-mongo与wow-elasticsearch在query包之外不引用查询包;me.ahoo.wow.api.query中的协议类型允许使用。test任务把被扫描的源码目录声明为输入,任一模块变化都会重新运行它。MetricDecoratorContractTest(:wow-core:test)与TracingDecoratorContractTest(:wow-opentelemetry:test)要求每个Metric*、Tracing*装饰器覆盖 SPI 中所有带默认实现的成员。否则调用会落到接口默认实现,绕过被装饰对象,例如会加载整个事件流的EventStore.existsRequestId。ElasticsearchEventStoreContractTest对 Elasticsearch 事件存储的existsRequestId做同样检查。每个测试都有一份KNOWN_GAPS清单,自 9.2.3 起为空。新增缺口会失败,已修复但仍留在清单里的缺口也会失败;只有同时写明负责移除它的工作项,才能加入新条目。
线上格式黄金样本
v9 的线上格式已冻结:滚动升级期间,9.2.x 节点和更新的 9.x 节点共用主题、流、存储和命令等待端点,双方都必须能读懂对方写出的内容。黄金样本测试锁住这些字节:每个测试把固定样本(me.ahoo.wow.tck.wire.WireSamples)交给生产代码序列化,与模块 src/test/resources/wire/v9/ 下提交的文件逐字节比较,再用生产代码的读取路径反序列化这个文件。
| 模块 | 测试 | 锁住的内容 |
|---|---|---|
wow-core | WireFormatGoldenTest | 命令消息 JSON(阶段等待和链式等待的消息头)、领域事件流 JSON、状态事件 JSON |
wow-kafka | KafkaWireFormatGoldenTest | 命令、事件、状态事件总线的 Kafka 记录:主题、分区、时间戳、键、记录头和值 |
wow-redis | RedisWireFormatGoldenTest | 命令、事件、状态事件总线的 Redis 流键和条目字段 |
wow-spring-boot-starter | WaitSignalWireFormatGoldenTest | 发往远端命令等待端点的 WaitSignal 请求体 |
样本带有运行时实际写入的消息头:command_operator、user_agent、remote_ip、local_first、trace_id、upstream_*,以及由真实等待计划写入的 command_wait_*。这些样本由 9.2.2 生成。黄金样本测试失败说明线上格式变了,不要为了让它通过而重新生成文件。修改黄金样本需要先有覆盖混部集群的设计决定;决定之后,用 WOW_GOLDEN_UPDATE=true 重新运行测试并审阅差异。
容器型集成测试
运行全部已注册集成任务:
./gradlew allIntegrationTest --stacktrace也可以只运行受影响 Adapter:
./gradlew :wow-mongo:integrationTest --stacktrace
./gradlew :wow-redis:integrationTest --stacktrace
./gradlew :wow-kafka:integrationTest --stacktrace
./gradlew :wow-elasticsearch:integrationTest --stacktrace
./gradlew :wow-it:integrationTest --stacktrace这些任务依赖 Docker/Testcontainers,并有意不挂到 check。:wow-it 验证 Wow 仓库内的集成组合,不能替代某个业务应用的配置、协议、恢复与安全门禁。
新旧版本混部测试
:wow-it 中的 MixedVersionClusterTest 检查滚动升级:已发布的示例服务镜像和由当前源码构建的示例服务共用一个 Kafka 和一个 MongoDB,加入同样的消费组,双方都必须能处理对方发出的内容。测试关闭本地优先路由,并按分区归属挑选聚合 ID,因此两个方向都会覆盖:
- 发给一个版本的命令由另一个版本处理,
PROCESSED等待结果回到发送方; - 一个版本追加的状态事件由另一个版本生成快照,
SNAPSHOT等待跨版本返回; - 一个版本上的 Saga(
CartSaga)处理另一个版本追加的领域事件; - 两个版本读到相同的聚合状态和事件流。
只有环境变量同时指明两个节点时测试才运行,所以 allIntegrationTest 会跳过它。Mixed-Version 工作流在拉取请求改动线上格式相关模块(wow-api、wow-core、wow-kafka、wow-redis、wow-mongo、wow-webflux、wow-spring、wow-spring-boot-starter)、示例服务、依赖版本或测试本身时运行它,并拉取已发布镜像(工作流中的 PREVIOUS_IMAGE)。如果只想在本地检查测试框架而不拉取该镜像,可以用当前构建的两份副本运行:
./gradlew :example-server:installDist
home=$PWD/example/example-server/build/install/example-server
WOW_MIXED_CURRENT_HOME=$home WOW_MIXED_PREVIOUS_HOME=$home \
./gradlew :wow-it:integrationTest --tests 'me.ahoo.wow.it.mixed.MixedVersionClusterTest'把 WOW_MIXED_PREVIOUS_HOME 换成 WOW_MIXED_PREVIOUS_IMAGE 即可像 CI 一样运行已发布镜像。示例服务没有 Redis 总线,所以 Redis 由线上格式测试覆盖,不在这个集群里。
覆盖率是分层证据
当前聚合与分层报告任务为:
./gradlew codeCoverageReport
./gradlew :code-coverage-report:localCoverageReport
./gradlew :code-coverage-report:contractCoverageReport
./gradlew :code-coverage-report:integrationCoverageReport
./gradlew :example-domain:jacocoTestCoverageVerification聚合 XML 输出到:
test/code-coverage-report/build/reports/jacoco/codeCoverageReport/codeCoverageReport.xml分层报告分别位于同名 localCoverageReport、contractCoverageReport 和 integrationCoverageReport 目录。PR 上传 local、contract 与 integration flag:变更的 main 代码须覆盖 90%(Codecov patch 状态),每个发布的 Kotlin 模块是一个 Codecov component,按这三个 flag 计算,覆盖率下降不得超过 1%。main 或手动触发的 Codecov 工作流用 codeCoverageReport 上传 full flag,并同时上传 contract 与 integration 层,使它们在 main 上也有数据。
:example-domain、:example-transfer-domain 和 :wow-compensation-domain 当前各自配置 0.8 的 Jacoco verification 下限。该阈值仅在显式运行对应的 jacocoTestCoverageVerification 任务时执行;这些模块的 check 与当前 CI 工作流都没有自动挂载 verification task。它是可选择执行的仓库门禁,不是 Wow 对业务应用覆盖率的保证。覆盖率只表示执行过代码,不能替代事件、状态、拒绝和恢复断言。
基准分三种用途
| 用途 | 入口 | 可以得出的结论 |
|---|---|---|
| Smoke | benchmarkSmoke | 选定 JMH jar 与路径可以编译、启动并完成 |
| Quick | benchmarkQuick* | 当前机器上的有边界回归线索 |
| Baseline / confirmation | benchmarkBaseline*、benchmarkConfirm* | 在匹配方法、参数、fork 与环境下可比较的证据 |
Smoke 不是性能报告,Quick 不是生产容量模型,隔离组件结果也不是框架端到端吞吐承诺。
PR 安全检查
./gradlew :wow-benchmarks:test :wow-benchmarks:benchmarkSmoke --stacktrace这与当前 Benchmark Smoke CI 工作流一致。根别名也可用:
./gradlew benchmarkSmoke完成信号是选定路径执行成功,不是产生或更新性能基线。
快速回归与诊断
框架 E2E 快速报告:
./gradlew :wow-benchmarks:benchmarkQuickE2E \
:wow-benchmarks:generateBenchmarkReport批量命令写入的成对工作负载:
./gradlew :wow-benchmarks:benchmarkQuickBatchE2E \
:wow-benchmarks:generateBatchBenchmarkReport需要定位瓶颈时,按层选择任务,而不是一次跑完整 catalog:
./gradlew :wow-benchmarks:benchmarkQuickComponent
./gradlew :wow-benchmarks:benchmarkQuickWebFlux -PbenchmarkQuickWebFluxThreads=1
./gradlew :wow-benchmarks:benchmarkQuickInfrastructureE2EWebFlux suite 不启动真实 Netty server。当前 benchmarkQuickInfrastructureE2E 同时包含 Redis 与 Mongo workload,因此本地 Redis 和 MongoDB 都是 required services,缺少任一服务都不满足该 suite 的运行条件。报告必须保留工作负载、线程、JVM、服务和源码 provenance,不能把层间数字直接横向解释。
改动框架热路径的工作,在同一台机器上对重构目录跑两次并分别加标签:改动前一次,改动后一次。benchmarkQuickRefactorHotPaths 不需要外部服务,覆盖事件分发(1 个与 8 个处理器、指标开与关、内存总线与本地优先总线)、本地优先的命令发送、命令 HTTP 入口(请求头 appender、整路由分发)、每条命令的聚合处理,以及事件流查询的结果交付。benchmarkQuickRefactorTransport 测 Redis Streams 的接收与确认,需要基准用的 Redis。Kafka 的接收与确认(KafkaEventReceiveAckBenchmark)不在 Gradle suite 中,因为基准还没有 Kafka 服务;用 JMH jar 对一个 broker 运行。
./gradlew :wow-benchmarks:benchmarkQuickRefactorHotPaths \
:wow-benchmarks:generateQuickRefactorHotPathsBenchmarkReport \
-PbenchmarkRefactorReportLabel=9.2.2-baseline正式回归证据
对精确的框架 E2E 工作负载建立可比较证据:
./gradlew :wow-benchmarks:benchmarkBaselineE2E --no-parallel
./gradlew :wow-benchmarks:benchmarkComparebenchmarkCompare 的阈值越界只是回归或改进候选。用相同 JVM、线程、参数、fork、预热、测量和 profiler 对受影响方法运行 benchmarkConfirmE2E 后,才能形成确认结论。
updateBenchmarkBaseline 只接受当前 clean HEAD 产生的 clean manifest。不要在脏工作树、不同服务配置或缺少 manifest 时更新基线。
在 CI 中做 A/B
Benchmark A/B(.github/workflows/benchmark-ab.yml)在 GitHub runner 上比较两个 ref,不占用本地机器。手动触发时给出 base ref(默认 main)、head ref、一个或多个 include 正则,以及 profile:
gh workflow run benchmark-ab.yml -f base=main -f head=my-branch \
-f include=CommandIdComponentBenchmark -f profile=quick它为每个 ref 只构建一次 JMH jar,并按提交与基准源码、构建文件的哈希缓存,同一组提交再次做 A/B 时跳过 Gradle 构建。随后把选中的基准分到矩阵 job 中。同一 job 内 base 与 head 的 fork 在同一台 runner 上交替运行(base-head、head-base……),抵消 runner 差异与漂移。每个 fork 都带 -prof gc。可选输入可覆盖 @Param(name=v1,v2;…)、线程数、噪声阈值(默认 3%),并可给出要评论的 PR 编号。benchmark.infrastructure 下的基准需要 Redis、MongoDB、Elasticsearch 或 Kafka,会被跳过。
| Profile | 每侧 fork | 预热、测量 | 默认 split | 典型耗时 |
|---|---|---|---|---|
quick | 最多 3 个 | 2 × 2 s、3 × 2 s | class | 单个类约 10–15 分钟 |
gate | 最多 8 个 | 3 × 3 s、5 × 3 s | params | 约 10–25 分钟,同时最多 16 个 job |
split 决定 job 粒度:class、method,或 params(每个方法与 @Param 组合一个 job,用 JMH -p 固定该组合)。默认值 auto 对 quick 用 class,对 gate 用 params。过去对含多个方法与参数的类做 gate,所有组合在一个 job 内串行运行(EventDispatchComponentBenchmark 约 130 分钟);按参数拆分后 8 个组合并行,A/A gate 运行约 11 分钟。plan 步骤在运行摘要中列出 job 数,同时最多运行 16 个(GitHub Free 每个账号最多 20 个并发 job),超过 48 个时给出警告,超过 GitHub 矩阵上限 256 个时失败,此时请收窄 include 或固定参数。
job 在其测量的每一行都有结论后提前停止。从第 2 轮到倒数第 2 轮,每轮结束后按目前的迭代计算两侧区间,置信度比报告更严格:把 JMH 99.9% 区间剩下的 0.1% 平均分给 job 可能做的提前检查(Bonferroni;gate 6 次,quick 1 次)。一行在以下情况视为已有结论:
- separated:两个区间不重叠且 |Δ| 达到阈值。报告用的 99.9% 区间更窄,因此报告给出相同结论;
- noise:即使取两个区间的最远端,差值仍小于阈值,更多 fork 也无法显示这么大的变化。阈值为 0% 时不会因噪声停止。
其他情况继续下一轮,直到 profile 的 fork 上限。判定规则本身不变:报告合并每行实际运行的 fork。
运行摘要中的报告列出 base 与 head 的 score ± error(合并该侧所有 fork,JMH 的 99.9% 区间)、Δ%、结论、该行每侧使用的 fork 数及其 job 停止的原因(separated、noise 或 max forks)、B/op、每次迭代的 GC 毫秒数,以及本次运行的总耗时。只有两个区间不重叠且 |Δ| 达到阈值时才标记 faster 或 slower;区间重叠即为噪声。给 PR 加 benchmark-ab 标签会以 quick profile 运行该 PR 改动的 JMH 类(head 对比其 merge base),并把报告评论到 PR。
如何读取历史报告
wow-benchmarks/results/reports/ 中的报告绑定于生成它们的源码、运行规格、机器、JVM 和服务配置。它们可以作为限定条件下的历史证据或调查起点,但不是跨版本、跨机器、跨存储的普适承诺。
遵守三条规则:
- 不手工改报告行或 frontier JSON,使用对应生成任务;
- 不用 Quick 点估计宣称正式吞吐变化;
- 不用组件或模拟 I/O 结果宣称生产端到端容量。
CI 与本地证据对应关系
| 工作流 | 当前命令 |
|---|---|
Local Test | allLocalTest + localCoverageReport |
Contract Test | allContractTest + contractCoverageReport |
Integration Test | allIntegrationTest + integrationCoverageReport |
Mixed-Version | :example-server:installDist + 使用已发布镜像运行 :wow-it:integrationTest --tests 'me.ahoo.wow.it.mixed.MixedVersionClusterTest' |
Benchmark Smoke | :wow-benchmarks:test + :wow-benchmarks:benchmarkSmoke |
Benchmark A/B(手动触发或 benchmark-ab 标签) | 每个 ref 运行 :wow-benchmarks:jmhJar,再用 JMH jar 交替运行 fork |
Codecov | codeCoverageReport |
本地验证应按变更风险选择这些层。CI 只是另一环境中的新证据;本地通过、CI 通过、应用发布和生产验证仍是不同完成条件。