test(seq): 三个压力测试 —— 超长序列 / 100 工具并行 / 串行降级

与单元判据的分工:单元判据钉住**语义**(一条路径对不对);压力测试钉住
**规模下的不变量**。沿用仓内既有范式(media/soak_test.go):
testing.Short() 跳过 + 独立 -run 跑。

① 超长序列
   · 解析 10 / 100 / 1000 组(250KB 文本):6.8ms,无硬上限误报
   · 执行 200 组 × 5 工具 = 1000 次调用:2.0ms
     断言:每工具恰好被调 nGroups 次(无遗漏/重复)、结果含**最后一组**
     —— 组间串行在规模下仍成立

② 100 工具组内并行
   · 100 工具全声明并发安全 ⇒ 11ms,完成顺序**确实被打乱**(判据会校验
     这一点,否则它测不到并发)
   · 断言每个槽拿到**自己**的结果(并发下若按完成顺序合并就会错位)

③ 串行降级
   · 50 个工具里**一个**未声明并发安全 ⇒ 整批退回串行,
     完成顺序严格等于声明序(106ms vs 并发的 11ms,降级确实生效)

★ 压力测试第一次跑就抓到一个**真实分层缺陷**:
「含非并发安全工具则整批串行」这条规则**只在上层 runGroup 实现**,
而引擎层 execGroup 只信 g.Parallel 字段 ⇒ 任何人直接调 execGroup
都会拿到不受约束的并发。
已修:降级判据下沉到引擎层,新增 batchCanRun(g, runner),
toolRunner 增加 parallelSafe 方法(生产路径行为不变,只是把判据
放到了它本该在的层)。

过程中压测自身也暴露了两个测试缺陷(都修了):
· fixture 让 100 个工具写同一个标量槽 o,被静态校验正确拦下
  ("组内并行下同名写入是数据竞争")—— 压测不该去撞这条规则;
· ★ e2eRunner.called 是无锁 append,100 工具并发时 -race 报出**真竞态**
  (不是误报)—— 加锁 + 提供 calledSnapshot 供断言。

另:建序列与跑序列原本用了**不同 plugin 实例**(序列存在实例的 store 里,
换实例就读不到自己刚建的),已改为同一实例。

回归:go test -race ./internal/plugins/seq 全绿;go test ./internal/... 全绿。
This commit is contained in:
JianFeeeee
2026-09-27 14:16:09 +08:00
parent 8ca28eb071
commit b1b96b788d
4 changed files with 409 additions and 3 deletions

View File

@ -17,6 +17,29 @@ import (
// 插值判据就成了摆设——它永远"通过")。
type toolRunner interface {
call(name string, args map[string]interface{}) (string, error)
// parallelSafe 报告工具是否**声明**可并发。
//
// ⚠️ 引擎层(execGroup)必须有它:否则"含非并发安全工具则整批串行"
// 这条规则只在上层(runGroup)实现 —— 任何人直接调 execGroup 都会
// 拿到不受约束的并发。压力测试 TestStress_ParallelNotSafeFallsBackToSerial
// 正是为此而写(它第一次跑就抓到了这个分层缺陷)。
parallelSafe(name string) bool
}
// batchCanRun 并发执行本组工具的判据。
//
// 规则:**全部**工具都声明并发安全才并发;一个不安全就**整批**串行。
// 不做部分并发 —— 收益不抵其不可预测性。
func batchCanRun(g Group, runner toolRunner) bool {
if !g.Parallel || len(g.Tools) <= 1 || runner == nil {
return false
}
for _, t := range g.Tools {
if !runner.parallelSafe(t.Tool) {
return false
}
}
return true
}
// errToolNotFound 表示「工具不存在」(未注册 / 插件未加载、已卸载或崩溃)。
@ -83,7 +106,7 @@ func execGroup(g Group, args map[string]interface{}, runner toolRunner) (GroupRe
}
// ② 执行
if g.Parallel && n > 1 {
if batchCanRun(g, runner) {
var wg sync.WaitGroup
for i := 0; i < n; i++ {
wg.Add(1)