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

@ -4,7 +4,9 @@ import (
"encoding/json"
"os"
"strings"
"sync"
"testing"
"time"
)
// 阶段 P4 端到端:真实 Plugin + 真实工具执行,串通
@ -21,14 +23,24 @@ import (
// e2eRunner 是真实执行面:它按名字查工具声明、真的返回结果。
type e2eRunner struct {
defs map[string]toolDefInfo
defs map[string]toolDefInfo
// mu 保护 called:组内并发时多个 goroutine 同时 append,
// 无锁会**真的**触发 -race(压测首次跑就报出来了)。
mu sync.Mutex
called []string
// results 覆盖默认返回
results map[string]string
// delay 按工具名制造延迟(毫秒),用于**打乱完成顺序**——
// 并发下若按完成顺序合并,槽内容就会错位;压力测试需要能造出这种乱序。
delay map[string]int
}
func newE2ERunner(names ...string) *e2eRunner {
r := &e2eRunner{defs: map[string]toolDefInfo{}, results: map[string]string{}}
r := &e2eRunner{
defs: map[string]toolDefInfo{},
results: map[string]string{},
delay: map[string]int{},
}
for _, n := range names {
// 默认**不**声明并发安全 ⇒ 序列会整批退回串行(保守默认)
r.defs[n] = toolDefInfo{Name: n, Description: n}
@ -45,7 +57,12 @@ func (r *e2eRunner) markParallel(names ...string) {
}
func (r *e2eRunner) call(name string, args map[string]interface{}) (string, error) {
if d := r.delay[name]; d > 0 {
time.Sleep(time.Duration(d) * time.Millisecond)
}
r.mu.Lock()
r.called = append(r.called, name)
r.mu.Unlock()
if s, ok := r.results[name]; ok {
return s, nil
}
@ -337,3 +354,12 @@ func TestSeqRunDoesNotSilentlyTruncateSlot(t *testing.T) {
}
}
}
// calledSnapshot 返回调用记录的快照(加锁)。
func (r *e2eRunner) calledSnapshot() []string {
r.mu.Lock()
defer r.mu.Unlock()
out := make([]string, len(r.called))
copy(out, r.called)
return out
}