xfy 2cb10eb749 perf(e2e): 并行化 E2E 测试,从 ~2h 降至 ~102s
- testutil: 用 sync.Once 缓存 LollyImageAvailable 结果
- testutil: 原子计数器替代时间戳避免容器名竞态
- testutil: SetupProxyTest 接受 suffix 参数生成独立 Docker 网络
- testutil: CleanupProxyTest 显式调用 network.Remove() 清理
- testutil: 移除死代码 SetupProxyTestEnv/ProxyTestEnv
- testutil: HealthCheckWaitTimeout 30s→15s, DefaultTestTimeout 180s→120s
- e2e: 所有 107 个测试函数添加 t.Parallel()
- e2e: 替换 65 处硬编码 30*time.Second 为常量
- make: test-all 三类测试并行运行,显式 PID wait 收集退出码
- make: test-e2e 添加 -parallel 4 flag

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-28 13:19:46 +08:00

215 lines
5.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//go:build e2e
// Package testutil 提供 E2E 测试的工具函数。
//
// 包含统一的测试环境设置函数。
//
// 作者xfy
package testutil
import (
"context"
"net/http"
"testing"
"time"
"github.com/testcontainers/testcontainers-go"
)
// E2ETestEnv E2E 测试环境。
//
// 封装测试所需的资源和清理函数。
type E2ETestEnv struct {
Ctx context.Context
Network testcontainers.Network
NetworkName string
Pool *BackendPool
Lolly *LollyContainer
Client *http.Client
cleanup func()
}
// SetupE2ETest 设置 E2E 测试环境。
//
// 自动处理镜像检查、后端启动、lolly 启动和资源清理。
// 使用 t.Cleanup() 确保资源正确释放。
//
// 参数:
// - t: 测试对象
// - backendCount: 后端数量
// - cfgBuilder: 配置构建函数,接收后端池返回 YAML 配置
//
// 使用示例:
//
// env := testutil.SetupE2ETest(t, 2, func(pool *testutil.BackendPool) string {
// cfg := testutil.NewConfigBuilder().
// WithServer(":8080").
// WithProxy("/", pool.InternalAddresses())
// yaml, _ := cfg.Build()
// return yaml
// })
// defer env.Cleanup()
func SetupE2ETest(t *testing.T, backendCount int, cfgBuilder func(*BackendPool) string) *E2ETestEnv {
t.Helper()
ctx, cancel := context.WithTimeout(context.Background(), DefaultTestTimeout)
if !LollyImageAvailable(ctx) {
t.Skip("lolly:latest image not available, run 'make docker-build' first")
}
netObj, networkName, pool, err := SetupProxyTest(ctx, backendCount, t.Name())
if err != nil {
cancel()
t.Fatalf("Failed to setup proxy test: %v", err)
}
cfgYAML := cfgBuilder(pool)
lolly, err := StartLolly(ctx,
WithConfigYAML(cfgYAML),
WithNetwork(networkName),
)
if err != nil {
CleanupProxyTest(ctx, netObj, networkName, pool)
cancel()
t.Fatalf("Failed to start lolly: %v", err)
}
if err := lolly.WaitForHealthy(ctx, HealthCheckWaitTimeout); err != nil {
lolly.Terminate(ctx)
CleanupProxyTest(ctx, netObj, networkName, pool)
cancel()
t.Fatalf("Lolly not healthy: %v", err)
}
env := &E2ETestEnv{
Ctx: ctx,
Network: netObj,
NetworkName: networkName,
Pool: pool,
Lolly: lolly,
Client: CreateDefaultHTTPClient(),
}
env.cleanup = func() {
lolly.Terminate(ctx)
CleanupProxyTest(ctx, netObj, networkName, pool)
cancel()
}
t.Cleanup(env.Cleanup)
return env
}
// SetupE2ETestWithTimeout 设置带自定义超时的 E2E 测试环境。
func SetupE2ETestWithTimeout(t *testing.T, backendCount int, timeout time.Duration, cfgBuilder func(*BackendPool) string) *E2ETestEnv {
t.Helper()
ctx, cancel := context.WithTimeout(context.Background(), timeout)
if !LollyImageAvailable(ctx) {
t.Skip("lolly:latest image not available, run 'make docker-build' first")
}
netObj, networkName, pool, err := SetupProxyTest(ctx, backendCount, t.Name())
if err != nil {
cancel()
t.Fatalf("Failed to setup proxy test: %v", err)
}
cfgYAML := cfgBuilder(pool)
lolly, err := StartLolly(ctx,
WithConfigYAML(cfgYAML),
WithNetwork(networkName),
)
if err != nil {
CleanupProxyTest(ctx, netObj, networkName, pool)
cancel()
t.Fatalf("Failed to start lolly: %v", err)
}
if err := lolly.WaitForHealthy(ctx, HealthCheckWaitTimeout); err != nil {
lolly.Terminate(ctx)
CleanupProxyTest(ctx, netObj, networkName, pool)
cancel()
t.Fatalf("Lolly not healthy: %v", err)
}
env := &E2ETestEnv{
Ctx: ctx,
Network: netObj,
NetworkName: networkName,
Pool: pool,
Lolly: lolly,
Client: CreateDefaultHTTPClient(),
}
env.cleanup = func() {
lolly.Terminate(ctx)
CleanupProxyTest(ctx, netObj, networkName, pool)
cancel()
}
t.Cleanup(env.Cleanup)
return env
}
// Cleanup 手动清理资源。
func (e *E2ETestEnv) Cleanup() {
if e.cleanup != nil {
e.cleanup()
e.cleanup = nil
}
}
// HTTPURL 返回 lolly HTTP 地址。
func (e *E2ETestEnv) HTTPURL() string {
return e.Lolly.HTTPBaseURL()
}
// HTTPSURL 返回 lolly HTTPS 地址。
func (e *E2ETestEnv) HTTPSURL() string {
return e.Lolly.HTTPSBaseURL()
}
// SetupSSLTest 设置 SSL 测试环境。
//
// 用于 SSL/TLS 测试场景,自动生成证书。
func SetupSSLTest(t *testing.T, cfgBuilder func() string) (*LollyContainer, string, string) {
t.Helper()
ctx := context.Background()
if !LollyImageAvailable(ctx) {
t.Skip("lolly:latest image not available, run 'make docker-build' first")
}
// 生成自签名证书
certPath, keyPath, cleanup, err := GenerateSelfSignedCert(t.TempDir())
if err != nil {
t.Fatalf("Failed to generate certificate: %v", err)
}
t.Cleanup(cleanup)
cfgYAML := cfgBuilder()
lolly, err := StartLolly(ctx,
WithConfigYAML(cfgYAML),
WithCert(certPath, keyPath),
)
if err != nil {
t.Fatalf("Failed to start lolly: %v", err)
}
t.Cleanup(func() { lolly.Terminate(ctx) })
if err := lolly.WaitForHealthy(ctx, HealthCheckWaitTimeout); err != nil {
t.Fatalf("Lolly not healthy: %v", err)
}
return lolly, certPath, keyPath
}