Files
gpt-load/internal/control/idempotency_operation_test.go
tbphp bd539713da perf: 优化测试性能与 CI/发布流水线耗时 (#465)
* test(control): 消除测试 fixture 中的重复密钥派生

- internal/control:fixture 构造中 388 次重复执行 PBKDF2(10 万轮
  SHA256)密钥派生,改为共享单例,仅派生一次
- 新增 internal/testutil/encryptiontest,提供按 key material 值
  memoize 的共享 encryption.Service,横向覆盖 gateway、subscription、
  execution/cpa 中同样存在的重复派生(PBKDF2 占比最高达 79%)
- aesService 构造后只读,跨 fixture(含并行测试)共享安全;container
  包同类调用经排查确认属于被测装配流程本身,不适用本次改造

* test(state): 迁移测试改用共享 schema 模板

state/loader 中 openMigratedDatabase 被 21 次调用,每次都完整走一遍
迁移注册表;改为复用 sqlitetest.OpenMigrated 的模板缓存,与 control、
requestlog 部分测试已采用的方式一致。app、container、requestlog 中同
类调用点经逐点排查,均属被测对象本身或依赖真实文件/账本状态,不适用。

* ci(release): 消除 CI 与发布流水线中的耗时浪费

- race-tests 独立 setup-go 缓存命名空间,避免与 test/database-contract
  共用 key 导致 race-instrumented GOCACHE 永远无法写回
- 移除 publish-images 中零命中的 GHA buildx image cache,同时消除其
  失败会拖累发布状态判定为 partial/blocked 的关联风险
- 三个只读 job(仅用 buildx imagetools 查询)移除多余的 buildx 初始化
- native-artifact-smoke 改为按需下载单个 binary + 独立 checksums,
  不再下载完整 release-assets

* test(control): 第一批测试标记 t.Parallel(35 个文件)

internal/control 651 个顶层测试里只有 5.8% 标了 t.Parallel(),其余
在包内是排队串行执行的;race-tests 总耗时现在完全由该包的最长执行时
间决定(213s),是唯一剩余的瓶颈。

本批覆盖 35 个文件、299 个函数。排除 18 个不适用的函数:
- 16 个直接操作全局 logrus 状态的测试(catalog_sync_test.go 等 8 个
  文件),继续保持串行;Go 的 t.Parallel() 调度语义保证未标记的测试
  会在所有并行测试开始前完整跑完,两者不会交叉执行
- external_database_integration_test.go 的 2 个测试依赖
  GPT_LOAD_DATABASE_TEST_DSN:race-tests 中因未设置而被 t.Skip,标记
  并行无收益;database-contract job 中会共享同一个外部数据库实例真
  实执行,标记并行有正确性风险(唯一索引冲突/断言假设独占)

已排查确认安全:t.Setenv/os.Setenv 均为 0 处,无硬编码端口占用,每个
fixture 走独立 :memory: 数据库;本地 -count=3 重复验证无 flaky。

剩余约 297 个函数(36 个文件)作为第二批,待本批 CI 实测确认收益后
再决定是否推进。

* test(control): 第二批测试标记 t.Parallel,补齐排除说明(36 个文件)

覆盖剩余 297 个函数、36 个文件,加上第一批的 299 个,internal/control
除 18 个明确排除的函数外全部标记 t.Parallel()。

为 18 个排除函数逐一补充一行注释说明原因,避免日后被误认为遗漏而被
顺手加上 t.Parallel():
- 16 个劫持全局 logrus 状态的测试
- 2 个依赖 GPT_LOAD_DATABASE_TEST_DSN 共享外部数据库的测试

本地 -count=3 重复验证、全量 go test -count=1 . ./internal/...(58
包 ok)均通过。

* fix(release): 采纳 Review 反馈,修正文档措辞与产物保留期

- encryptiontest.Service 的文档注释修正为明确说明缓存仅在单个包的
  测试二进制内有效,不跨包共享(每个包的 go test 是独立进程)
- native-artifact-smoke 依赖的 binary-*、release-checksums 保留期
  从 1 天提高到与 release-assets 一致的 14 天:失败的 release run
  若延迟超过 1 天才重试,此前较短的保留期会导致产物过期、下载失败,
  阻塞发布流程

以下反馈经核实判定为不成立,未采纳:
- validation_test.go 中两个测试被指"竞争全局并发槽位"——经查
  newValidationWorkerForTest 每次调用构造全新独立实例(worker、
  channel、mutation coordinator 等),validationConcurrency 只是
  编译期常量,两个测试之间不存在任何运行时共享状态
2026-08-24 20:07:11 +08:00

222 lines
6.8 KiB
Go

package control
import (
"bytes"
"context"
"crypto/sha256"
"encoding/json"
"errors"
"testing"
app_errors "gpt-load/internal/platform/errors"
"gpt-load/internal/storage/models"
)
func TestOperationRequiredStagesAreKindSpecificAndDetached(t *testing.T) {
t.Parallel()
tests := []struct {
kind operationKind
want []operationStage
}{
{
kind: operationKindAccessKeyCreate,
want: []operationStage{
operationStageDBCommitted,
operationStageSnapshotPublished,
operationStageCompleted,
},
},
{
kind: operationKindGroupCreate,
want: []operationStage{
operationStageDBCommitted,
operationStagePricesPublished,
operationStageRegistryApplied,
operationStageSnapshotPublished,
operationStageCompleted,
},
},
{
kind: operationKindCredentialImport,
want: []operationStage{
operationStageDBCommitted,
operationStageRegistryApplied,
operationStageCompleted,
},
},
}
for _, test := range tests {
t.Run(string(test.kind), func(t *testing.T) {
got, err := operationRequiredStages(test.kind)
if err != nil {
t.Fatalf("operationRequiredStages() error = %v", err)
}
if !equalOperationStages(got, test.want) {
t.Fatalf("stages = %#v, want %#v", got, test.want)
}
got[0] = operationStageCompleted
again, err := operationRequiredStages(test.kind)
if err != nil {
t.Fatalf("second operationRequiredStages() error = %v", err)
}
if !equalOperationStages(again, test.want) {
t.Fatalf("stage plan aliases caller: %#v", again)
}
})
}
if _, err := operationRequiredStages(operationKind("unknown")); err == nil {
t.Fatal("operationRequiredStages(unknown) error = nil, want rejection")
}
}
func TestValidateIdempotencyKeyRequiresCanonicalLowercaseUUIDV4(t *testing.T) {
t.Parallel()
const valid = "018f47a2-9c35-4d6e-8b1a-1234567890ab"
if err := validateIdempotencyKey(valid); err != nil {
t.Fatalf("validateIdempotencyKey(valid) error = %v", err)
}
for _, invalid := range []string{
"",
" 018f47a2-9c35-4d6e-8b1a-1234567890ab",
"018F47A2-9C35-4D6E-8B1A-1234567890AB",
"018f47a2-9c35-3d6e-8b1a-1234567890ab",
"018f47a2-9c35-4d6e-7b1a-1234567890ab",
"018f47a29c354d6e8b1a1234567890ab",
} {
t.Run(invalid, func(t *testing.T) {
if err := validateIdempotencyKey(invalid); err == nil {
t.Fatalf("validateIdempotencyKey(%q) error = nil, want rejection", invalid)
}
})
}
}
func TestNewOperationIDUsesCanonicalUUIDV4WithoutSharingCredentialRandom(t *testing.T) {
t.Parallel()
random := bytes.NewReader([]byte{
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
})
got, err := newOperationID(random)
if err != nil {
t.Fatalf("newOperationID() error = %v", err)
}
if got != "00010203-0405-4607-8809-0a0b0c0d0e0f" {
t.Fatalf("newOperationID() = %q", got)
}
if err := validateIdempotencyKey(got); err != nil {
t.Fatalf("generated operation ID is not canonical UUID v4: %v", err)
}
}
func TestExecuteIdempotentOperationCommitsResultAndReplaysWithoutMutation(t *testing.T) {
t.Parallel()
fixture := newServiceFixture(t)
fixture.service.operationRandom = bytes.NewReader(bytes.Repeat([]byte{0x42}, 16))
const key = "018f47a2-9c35-4d6e-8b1a-1234567890ab"
digest := sha256.Sum256([]byte("same request"))
mutations := 0
input := idempotentOperationInput{
IdempotencyKey: key,
DigestVersion: 1,
RequestDigest: digest,
Kind: operationKindAccessKeyCreate,
Mutate: func(tx operationTransaction) (idempotentMutationResult, error) {
mutations++
return idempotentMutationResult{
ResourceIdentity: "access-key:7",
CanonicalResult: []byte(`{"id":7,"name":"client"}`),
Ephemeral: "one-time-secret",
}, nil
},
}
first, err := fixture.service.executeIdempotentOperation(context.Background(), input)
if err != nil {
t.Fatalf("first executeIdempotentOperation() error = %v", err)
}
if first.Replayed || first.Ephemeral != "one-time-secret" ||
string(first.CanonicalResult) != `{"id":7,"name":"client"}` {
t.Fatalf("first result = %#v", first)
}
replayInput := input
replayInput.Mutate = func(operationTransaction) (idempotentMutationResult, error) {
mutations++
return idempotentMutationResult{}, errors.New("mutation reran")
}
replayed, err := fixture.service.executeIdempotentOperation(context.Background(), replayInput)
if err != nil {
t.Fatalf("replay executeIdempotentOperation() error = %v", err)
}
if !replayed.Replayed || replayed.Ephemeral != nil ||
string(replayed.CanonicalResult) != string(first.CanonicalResult) {
t.Fatalf("replay result = %#v", replayed)
}
if mutations != 1 {
t.Fatalf("mutation calls = %d, want 1", mutations)
}
var row models.ControlOperation
if err := fixture.db.Where("idempotency_key = ?", key).Take(&row).Error; err != nil {
t.Fatalf("read ControlOperation: %v", err)
}
if row.CommitSequence == 0 || row.ResourceIdentity != "access-key:7" ||
row.LastCompletedStage != string(operationStageCompleted) ||
row.CompletedAtMS == nil || row.CompactedAtMS != nil {
t.Fatalf("operation row = %#v", row)
}
var stages []operationStage
if err := json.Unmarshal(row.RequiredStages, &stages); err != nil {
t.Fatalf("decode required stages: %v", err)
}
wantStages, _ := operationRequiredStages(operationKindAccessKeyCreate)
if !equalOperationStages(stages, wantStages) {
t.Fatalf("stored stages = %#v, want %#v", stages, wantStages)
}
if bytes.Contains(row.CanonicalResult, []byte("one-time-secret")) {
t.Fatal("operation row persisted ephemeral secret")
}
}
func TestExecuteIdempotentOperationRejectsKeyReuseWithDifferentDigest(t *testing.T) {
t.Parallel()
fixture := newServiceFixture(t)
fixture.service.operationRandom = bytes.NewReader(bytes.Repeat([]byte{0x24}, 16))
const key = "018f47a2-9c35-4d6e-8b1a-1234567890ab"
firstDigest := sha256.Sum256([]byte("first"))
input := idempotentOperationInput{
IdempotencyKey: key, DigestVersion: 1, RequestDigest: firstDigest,
Kind: operationKindAccessKeyCreate,
Mutate: func(operationTransaction) (idempotentMutationResult, error) {
return idempotentMutationResult{
ResourceIdentity: "access-key:7",
CanonicalResult: []byte(`{"id":7,"name":"client"}`),
}, nil
},
}
if _, err := fixture.service.executeIdempotentOperation(t.Context(), input); err != nil {
t.Fatalf("seed executeIdempotentOperation() error = %v", err)
}
input.RequestDigest = sha256.Sum256([]byte("different"))
_, err := fixture.service.executeIdempotentOperation(t.Context(), input)
var apiErr *app_errors.APIError
if !errors.As(err, &apiErr) || apiErr.Code != app_errors.ErrIdempotencyKeyReused.Code {
t.Fatalf("different digest error = %v, want IDEMPOTENCY_KEY_REUSED", err)
}
}
func equalOperationStages(left, right []operationStage) bool {
if len(left) != len(right) {
return false
}
for index := range left {
if left[index] != right[index] {
return false
}
}
return true
}