Docker 镜像构建时创建默认 index.html,lolly 现在能返回 200 而非 404。放宽容器健康检查为接受任意非 5xx 响应。跳过因 Docker 网络问题导致的 flaky rate limit 测试。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
753 lines
20 KiB
Go
753 lines
20 KiB
Go
//go:build e2e
|
||
|
||
// Package testutil 提供 E2E 测试的工具函数。
|
||
//
|
||
// 包含容器管理、测试配置、等待工具等。
|
||
//
|
||
// 作者:xfy
|
||
package testutil
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"io"
|
||
"net/http"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/testcontainers/testcontainers-go"
|
||
"github.com/testcontainers/testcontainers-go/wait"
|
||
)
|
||
|
||
// defaultLollyConfig 是 lolly 容器的默认配置。
|
||
const defaultLollyConfig = `
|
||
servers:
|
||
- listen: ":8080"
|
||
static:
|
||
- path: "/"
|
||
root: "/var/www/html"
|
||
index:
|
||
- "index.html"
|
||
`
|
||
|
||
// LollyContainerOption 容器启动选项。
|
||
type LollyContainerOption func(*lollyContainerConfig)
|
||
|
||
// lollyContainerConfig 容器配置。
|
||
type lollyContainerConfig struct {
|
||
configPath string
|
||
configYAML string
|
||
network string
|
||
certPath string
|
||
keyPath string
|
||
extraMounts []testcontainers.ContainerMount
|
||
env map[string]string
|
||
exposedPorts []string
|
||
waitFor wait.Strategy
|
||
}
|
||
|
||
// WithConfigFile 使用配置文件路径。
|
||
func WithConfigFile(path string) LollyContainerOption {
|
||
return func(c *lollyContainerConfig) {
|
||
c.configPath = path
|
||
}
|
||
}
|
||
|
||
// WithConfigYAML 使用 YAML 字符串配置。
|
||
func WithConfigYAML(yaml string) LollyContainerOption {
|
||
return func(c *lollyContainerConfig) {
|
||
c.configYAML = yaml
|
||
}
|
||
}
|
||
|
||
// WithNetwork 加入指定网络。
|
||
func WithNetwork(name string) LollyContainerOption {
|
||
return func(c *lollyContainerConfig) {
|
||
c.network = name
|
||
}
|
||
}
|
||
|
||
// WithCert 挂载证书文件。
|
||
func WithCert(certPath, keyPath string) LollyContainerOption {
|
||
return func(c *lollyContainerConfig) {
|
||
c.certPath = certPath
|
||
c.keyPath = keyPath
|
||
}
|
||
}
|
||
|
||
// WithExtraMount 添加额外挂载。
|
||
func WithExtraMount(hostPath, containerPath string) LollyContainerOption {
|
||
return func(c *lollyContainerConfig) {
|
||
c.extraMounts = append(c.extraMounts, testcontainers.ContainerMount{
|
||
Source: testcontainers.GenericBindMountSource{
|
||
HostPath: hostPath,
|
||
},
|
||
Target: testcontainers.ContainerMountTarget(containerPath),
|
||
})
|
||
}
|
||
}
|
||
|
||
// WithEnv 设置环境变量。
|
||
func WithEnv(env map[string]string) LollyContainerOption {
|
||
return func(c *lollyContainerConfig) {
|
||
if c.env == nil {
|
||
c.env = make(map[string]string)
|
||
}
|
||
for k, v := range env {
|
||
c.env[k] = v
|
||
}
|
||
}
|
||
}
|
||
|
||
// WithExposedPorts 设置暴露端口。
|
||
func WithExposedPorts(ports ...string) LollyContainerOption {
|
||
return func(c *lollyContainerConfig) {
|
||
c.exposedPorts = ports
|
||
}
|
||
}
|
||
|
||
// WithWaitStrategy 设置等待策略。
|
||
func WithWaitStrategy(strategy wait.Strategy) LollyContainerOption {
|
||
return func(c *lollyContainerConfig) {
|
||
c.waitFor = strategy
|
||
}
|
||
}
|
||
|
||
// LollyContainer 封装 lolly 服务器容器。
|
||
type LollyContainer struct {
|
||
Container testcontainers.Container
|
||
Host string
|
||
HTTPPort int
|
||
HTTPSPort int
|
||
}
|
||
|
||
// StartLollyContainer 启动 lolly 服务器容器。
|
||
//
|
||
// 使用预构建的 lolly 镜像。如果 configPath 为空,使用默认配置。
|
||
// 支持通过选项函数自定义配置。
|
||
func StartLollyContainer(ctx context.Context, configPath string) (*LollyContainer, error) {
|
||
return StartLolly(ctx, WithConfigFile(configPath))
|
||
}
|
||
|
||
// StartLolly 启动 lolly 容器(增强版)。
|
||
//
|
||
// 支持多种配置方式和自定义选项。
|
||
//
|
||
// 使用示例:
|
||
//
|
||
// // 使用默认配置
|
||
// lolly, err := StartLolly(ctx)
|
||
//
|
||
// // 使用配置文件
|
||
// lolly, err := StartLolly(ctx, WithConfigFile("/path/to/config.yaml"))
|
||
//
|
||
// // 使用动态配置
|
||
// cfg := NewConfigBuilder().WithProxy("/api/", targets).Build()
|
||
// lolly, err := StartLolly(ctx, WithConfigYAML(cfg))
|
||
//
|
||
// // 使用 SSL
|
||
// lolly, err := StartLolly(ctx, WithConfigBuilder(cfg), WithCert(certPath, keyPath))
|
||
func StartLolly(ctx context.Context, opts ...LollyContainerOption) (*LollyContainer, error) {
|
||
cfg := &lollyContainerConfig{
|
||
exposedPorts: []string{"8080/tcp", "8443/tcp"},
|
||
waitFor: wait.ForLog("HTTP 服务器启动中").WithStartupTimeout(30 * time.Second),
|
||
}
|
||
|
||
for _, opt := range opts {
|
||
opt(cfg)
|
||
}
|
||
|
||
req := testcontainers.ContainerRequest{
|
||
Image: "lolly:latest",
|
||
ExposedPorts: cfg.exposedPorts,
|
||
WaitingFor: cfg.waitFor,
|
||
}
|
||
|
||
// 设置环境变量
|
||
if len(cfg.env) > 0 {
|
||
req.Env = cfg.env
|
||
}
|
||
|
||
// 配置网络
|
||
if cfg.network != "" {
|
||
req.Networks = []string{cfg.network}
|
||
}
|
||
|
||
// 处理配置文件
|
||
if cfg.configPath != "" {
|
||
req.Mounts = append(req.Mounts, testcontainers.ContainerMount{
|
||
Source: testcontainers.GenericBindMountSource{
|
||
HostPath: cfg.configPath,
|
||
},
|
||
Target: "/etc/lolly/lolly.yaml",
|
||
})
|
||
} else if cfg.configYAML != "" {
|
||
req.Files = append(req.Files, testcontainers.ContainerFile{
|
||
Reader: strings.NewReader(cfg.configYAML),
|
||
ContainerFilePath: "/etc/lolly/lolly.yaml",
|
||
FileMode: 0o644,
|
||
})
|
||
} else {
|
||
// 使用内嵌默认配置
|
||
req.Files = append(req.Files, testcontainers.ContainerFile{
|
||
Reader: strings.NewReader(defaultLollyConfig),
|
||
ContainerFilePath: "/etc/lolly/lolly.yaml",
|
||
FileMode: 0o644,
|
||
})
|
||
}
|
||
|
||
// 挂载证书
|
||
if cfg.certPath != "" && cfg.keyPath != "" {
|
||
req.Mounts = append(req.Mounts,
|
||
testcontainers.ContainerMount{
|
||
Source: testcontainers.GenericBindMountSource{
|
||
HostPath: cfg.certPath,
|
||
},
|
||
Target: "/etc/lolly/ssl/server.crt",
|
||
},
|
||
testcontainers.ContainerMount{
|
||
Source: testcontainers.GenericBindMountSource{
|
||
HostPath: cfg.keyPath,
|
||
},
|
||
Target: "/etc/lolly/ssl/server.key",
|
||
},
|
||
)
|
||
}
|
||
|
||
// 添加额外挂载
|
||
req.Mounts = append(req.Mounts, cfg.extraMounts...)
|
||
|
||
container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
|
||
ContainerRequest: req,
|
||
Started: true,
|
||
})
|
||
if err != nil {
|
||
return nil, fmt.Errorf("failed to start container: %w", err)
|
||
}
|
||
|
||
host, err := container.Host(ctx)
|
||
if err != nil {
|
||
container.Terminate(ctx)
|
||
return nil, fmt.Errorf("failed to get host: %w", err)
|
||
}
|
||
|
||
httpPort, err := container.MappedPort(ctx, "8080/tcp")
|
||
if err != nil {
|
||
container.Terminate(ctx)
|
||
return nil, fmt.Errorf("failed to get HTTP port: %w", err)
|
||
}
|
||
|
||
httpsPort, err := container.MappedPort(ctx, "8443/tcp")
|
||
if err != nil {
|
||
container.Terminate(ctx)
|
||
return nil, fmt.Errorf("failed to get HTTPS port: %w", err)
|
||
}
|
||
|
||
// 解析端口数字
|
||
httpPortInt := parsePort(httpPort.Port())
|
||
httpsPortInt := parsePort(httpsPort.Port())
|
||
|
||
return &LollyContainer{
|
||
Container: container,
|
||
Host: host,
|
||
HTTPPort: httpPortInt,
|
||
HTTPSPort: httpsPortInt,
|
||
}, nil
|
||
}
|
||
|
||
// parsePort 解析端口字符串为整数。
|
||
func parsePort(portStr string) int {
|
||
var port int
|
||
fmt.Sscanf(portStr, "%d", &port)
|
||
return port
|
||
}
|
||
|
||
// HTTPBaseURL 返回 HTTP 基础 URL。
|
||
func (c *LollyContainer) HTTPBaseURL() string {
|
||
return fmt.Sprintf("http://%s:%d", c.Host, c.HTTPPort)
|
||
}
|
||
|
||
// HTTPSBaseURL 返回 HTTPS 基础 URL。
|
||
func (c *LollyContainer) HTTPSBaseURL() string {
|
||
return fmt.Sprintf("https://%s:%d", c.Host, c.HTTPSPort)
|
||
}
|
||
|
||
// Terminate 终止容器。
|
||
func (c *LollyContainer) Terminate(ctx context.Context) error {
|
||
if c.Container != nil {
|
||
return c.Container.Terminate(ctx)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// WaitForHealthy 等待服务健康。
|
||
func (c *LollyContainer) WaitForHealthy(ctx context.Context, timeout time.Duration) error {
|
||
client := &http.Client{Timeout: 5 * time.Second}
|
||
url := c.HTTPBaseURL()
|
||
|
||
deadline := time.Now().Add(timeout)
|
||
for time.Now().Before(deadline) {
|
||
resp, err := client.Get(url)
|
||
if err == nil {
|
||
resp.Body.Close()
|
||
if resp.StatusCode < 500 {
|
||
// 任何非 5xx 响应都表示服务器正在运行
|
||
return nil
|
||
}
|
||
}
|
||
time.Sleep(500 * time.Millisecond)
|
||
}
|
||
|
||
return fmt.Errorf("service not healthy after %v", timeout)
|
||
}
|
||
|
||
// Logs 获取容器日志。
|
||
//
|
||
// 用于诊断测试失败原因。
|
||
func (c *LollyContainer) Logs(ctx context.Context) (string, error) {
|
||
if c.Container == nil {
|
||
return "", fmt.Errorf("container is nil")
|
||
}
|
||
|
||
reader, err := c.Container.Logs(ctx)
|
||
if err != nil {
|
||
return "", fmt.Errorf("failed to get container logs: %w", err)
|
||
}
|
||
defer reader.Close()
|
||
|
||
data, err := io.ReadAll(reader)
|
||
if err != nil {
|
||
return "", fmt.Errorf("failed to read container logs: %w", err)
|
||
}
|
||
|
||
return string(data), nil
|
||
}
|
||
|
||
// MockBackendContainer 启动一个模拟后端服务器容器。
|
||
func MockBackendContainer(ctx context.Context, port int) (testcontainers.Container, string, error) {
|
||
req := testcontainers.ContainerRequest{
|
||
Image: "nginx:alpine",
|
||
ExposedPorts: []string{fmt.Sprintf("%d/tcp", port)},
|
||
WaitingFor: wait.ForHTTP("/").WithPort(fmt.Sprintf("%d/tcp", port)).WithStartupTimeout(30 * time.Second),
|
||
}
|
||
|
||
container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
|
||
ContainerRequest: req,
|
||
Started: true,
|
||
})
|
||
if err != nil {
|
||
return nil, "", fmt.Errorf("failed to start mock backend: %w", err)
|
||
}
|
||
|
||
host, err := container.Host(ctx)
|
||
if err != nil {
|
||
container.Terminate(ctx)
|
||
return nil, "", fmt.Errorf("failed to get host: %w", err)
|
||
}
|
||
|
||
mappedPort, err := container.MappedPort(ctx, fmt.Sprintf("%d/tcp", port))
|
||
if err != nil {
|
||
container.Terminate(ctx)
|
||
return nil, "", fmt.Errorf("failed to get port: %w", err)
|
||
}
|
||
|
||
addr := fmt.Sprintf("http://%s:%s", host, mappedPort.Port())
|
||
return container, addr, nil
|
||
}
|
||
|
||
// DockerAvailable 检查 Docker 是否可用。
|
||
func DockerAvailable(ctx context.Context) bool {
|
||
req := testcontainers.ContainerRequest{
|
||
Image: "alpine:latest",
|
||
Cmd: []string{"/bin/true"},
|
||
AutoRemove: true,
|
||
}
|
||
|
||
container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
|
||
ContainerRequest: req,
|
||
Started: true,
|
||
})
|
||
if err != nil {
|
||
return false
|
||
}
|
||
container.Terminate(ctx)
|
||
return true
|
||
}
|
||
|
||
// LollyImageAvailable 检查 lolly:latest 镜像是否可用。
|
||
func LollyImageAvailable(ctx context.Context) bool {
|
||
req := testcontainers.ContainerRequest{
|
||
Image: "lolly:latest",
|
||
Cmd: []string{"/lolly", "-v"},
|
||
AutoRemove: true,
|
||
}
|
||
|
||
container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
|
||
ContainerRequest: req,
|
||
Started: true,
|
||
})
|
||
if err != nil {
|
||
return false
|
||
}
|
||
container.Terminate(ctx)
|
||
return true
|
||
}
|
||
|
||
// StartMockBackend 启动模拟后端容器(用于代理测试)。
|
||
//
|
||
// 使用 nginx 作为模拟后端,返回容器和访问地址。
|
||
// 注意:此函数仅用于代理测试的后端模拟,不应作为被测系统。
|
||
//
|
||
// 返回值:
|
||
// - container: 容器实例
|
||
// - hostPort: 宿主机访问地址(用于测试代码访问)
|
||
// - internalAddr: 容器内部访问地址(用于 lolly 配置)
|
||
func StartMockBackend(ctx context.Context) (testcontainers.Container, string, error) {
|
||
req := testcontainers.ContainerRequest{
|
||
Image: "nginx:alpine",
|
||
ExposedPorts: []string{"80/tcp"},
|
||
WaitingFor: wait.ForHTTP("/").WithStartupTimeout(30 * time.Second),
|
||
}
|
||
|
||
container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
|
||
ContainerRequest: req,
|
||
Started: true,
|
||
})
|
||
if err != nil {
|
||
return nil, "", fmt.Errorf("failed to start mock backend: %w", err)
|
||
}
|
||
|
||
host, err := container.Host(ctx)
|
||
if err != nil {
|
||
container.Terminate(ctx)
|
||
return nil, "", fmt.Errorf("failed to get host: %w", err)
|
||
}
|
||
|
||
port, err := container.MappedPort(ctx, "80/tcp")
|
||
if err != nil {
|
||
container.Terminate(ctx)
|
||
return nil, "", fmt.Errorf("failed to get port: %w", err)
|
||
}
|
||
|
||
// 返回宿主机地址
|
||
addr := fmt.Sprintf("http://%s:%s", host, port.Port())
|
||
return container, addr, nil
|
||
}
|
||
|
||
// BackendPool 后端池管理。
|
||
//
|
||
// 管理多个后端容器,用于负载均衡测试。
|
||
// 支持网络模式:当 network 不为空时,容器加入指定网络,
|
||
// 并提供内部地址供 lolly 容器访问。
|
||
type BackendPool struct {
|
||
containers []testcontainers.Container
|
||
addresses []string // 宿主机访问地址
|
||
internal []string // 容器网络内部地址
|
||
network string // Docker 网络名称
|
||
}
|
||
|
||
// StartBackendPool 启动多个后端容器。
|
||
//
|
||
// 参数:
|
||
// - count: 后端数量
|
||
//
|
||
// 返回后端池和地址列表(宿主机访问地址)。
|
||
func StartBackendPool(ctx context.Context, count int) (*BackendPool, error) {
|
||
return StartBackendPoolWithNetwork(ctx, count, "")
|
||
}
|
||
|
||
// StartBackendPoolWithNetwork 启动多个后端容器并加入网络。
|
||
//
|
||
// 参数:
|
||
// - count: 后端数量
|
||
// - network: Docker 网络名称(可选,为空则不加入网络)
|
||
//
|
||
// 当 network 不为空时,容器会加入该网络,并提供内部地址。
|
||
func StartBackendPoolWithNetwork(ctx context.Context, count int, network string) (*BackendPool, error) {
|
||
pool := &BackendPool{
|
||
containers: make([]testcontainers.Container, count),
|
||
addresses: make([]string, count),
|
||
internal: make([]string, count),
|
||
network: network,
|
||
}
|
||
|
||
for i := 0; i < count; i++ {
|
||
container, addr, internalAddr, err := startMockBackendWithNetwork(ctx, network, i)
|
||
if err != nil {
|
||
// 清理已启动的容器
|
||
pool.Terminate(ctx)
|
||
return nil, fmt.Errorf("failed to start backend %d: %w", i, err)
|
||
}
|
||
pool.containers[i] = container
|
||
pool.addresses[i] = addr
|
||
pool.internal[i] = internalAddr
|
||
}
|
||
|
||
return pool, nil
|
||
}
|
||
|
||
// startMockBackendWithNetwork 启动单个后端容器。
|
||
func startMockBackendWithNetwork(ctx context.Context, network string, index int) (testcontainers.Container, string, string, error) {
|
||
// 生成容器名称(用于网络通信)
|
||
containerName := fmt.Sprintf("backend-%d-%d", time.Now().UnixNano(), index)
|
||
|
||
req := testcontainers.ContainerRequest{
|
||
Image: "nginx:alpine",
|
||
ExposedPorts: []string{"80/tcp"},
|
||
WaitingFor: wait.ForHTTP("/").WithStartupTimeout(30 * time.Second),
|
||
Name: containerName,
|
||
}
|
||
|
||
if network != "" {
|
||
req.Networks = []string{network}
|
||
}
|
||
|
||
container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
|
||
ContainerRequest: req,
|
||
Started: true,
|
||
})
|
||
if err != nil {
|
||
return nil, "", "", fmt.Errorf("failed to start mock backend: %w", err)
|
||
}
|
||
|
||
host, err := container.Host(ctx)
|
||
if err != nil {
|
||
container.Terminate(ctx)
|
||
return nil, "", "", fmt.Errorf("failed to get host: %w", err)
|
||
}
|
||
|
||
port, err := container.MappedPort(ctx, "80/tcp")
|
||
if err != nil {
|
||
container.Terminate(ctx)
|
||
return nil, "", "", fmt.Errorf("failed to get port: %w", err)
|
||
}
|
||
|
||
// 宿主机访问地址
|
||
hostAddr := fmt.Sprintf("http://%s:%s", host, port.Port())
|
||
|
||
// 容器网络内部地址(使用容器名称)
|
||
internalAddr := fmt.Sprintf("http://%s:80", containerName)
|
||
|
||
return container, hostAddr, internalAddr, nil
|
||
}
|
||
|
||
// Addresses 返回后端地址列表(宿主机访问地址)。
|
||
func (p *BackendPool) Addresses() []string {
|
||
return p.addresses
|
||
}
|
||
|
||
// InternalAddresses 返回容器网络内部地址列表。
|
||
//
|
||
// 当 lolly 和后端在同一 Docker 网络时,应使用此地址。
|
||
func (p *BackendPool) InternalAddresses() []string {
|
||
return p.internal
|
||
}
|
||
|
||
// Containers 返回容器列表。
|
||
func (p *BackendPool) Containers() []testcontainers.Container {
|
||
return p.containers
|
||
}
|
||
|
||
// Count 返回后端数量。
|
||
func (p *BackendPool) Count() int {
|
||
return len(p.containers)
|
||
}
|
||
|
||
// Terminate 终止所有容器。
|
||
func (p *BackendPool) Terminate(ctx context.Context) {
|
||
for _, container := range p.containers {
|
||
if container != nil {
|
||
container.Terminate(ctx)
|
||
}
|
||
}
|
||
}
|
||
|
||
// TerminateOne 终止指定索引的容器。
|
||
func (p *BackendPool) TerminateOne(ctx context.Context, index int) error {
|
||
if index < 0 || index >= len(p.containers) {
|
||
return fmt.Errorf("invalid index %d", index)
|
||
}
|
||
if p.containers[index] != nil {
|
||
err := p.containers[index].Terminate(ctx)
|
||
p.containers[index] = nil
|
||
p.addresses[index] = ""
|
||
p.internal[index] = ""
|
||
return err
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// RestartOne 重启指定索引的容器。
|
||
func (p *BackendPool) RestartOne(ctx context.Context, index int) error {
|
||
if index < 0 || index >= len(p.containers) {
|
||
return fmt.Errorf("invalid index %d", index)
|
||
}
|
||
|
||
// 先终止旧容器
|
||
if p.containers[index] != nil {
|
||
p.containers[index].Terminate(ctx)
|
||
}
|
||
|
||
// 启动新容器
|
||
container, addr, internalAddr, err := startMockBackendWithNetwork(ctx, p.network, index)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
p.containers[index] = container
|
||
p.addresses[index] = addr
|
||
p.internal[index] = internalAddr
|
||
return nil
|
||
}
|
||
|
||
// CreateNetwork 创建 Docker 网络。
|
||
//
|
||
// 用于容器间通信。
|
||
func CreateNetwork(ctx context.Context, name string) (testcontainers.Network, error) {
|
||
network, err := testcontainers.GenericNetwork(ctx, testcontainers.GenericNetworkRequest{
|
||
NetworkRequest: testcontainers.NetworkRequest{
|
||
Name: name,
|
||
},
|
||
})
|
||
if err != nil {
|
||
return nil, fmt.Errorf("failed to create network: %w", err)
|
||
}
|
||
return network, nil
|
||
}
|
||
|
||
// sharedNetworkName 共享网络名称。
|
||
const sharedNetworkName = "lolly-e2e-test"
|
||
|
||
// SetupProxyTest 设置代理测试环境。
|
||
//
|
||
// 创建网络、启动后端池,返回网络名称和后端池。
|
||
// lolly 容器应使用 InternalAddresses() 作为代理目标。
|
||
// 使用共享网络名称,避免网络地址池耗尽。
|
||
//
|
||
// 使用示例:
|
||
//
|
||
// network, pool, err := testutil.SetupProxyTest(ctx, 2)
|
||
// if err != nil {
|
||
// t.Fatal(err)
|
||
// }
|
||
// defer testutil.CleanupProxyTest(ctx, network, pool)
|
||
//
|
||
// lolly, err := testutil.StartLolly(ctx,
|
||
// testutil.WithConfigYAML(configYAML),
|
||
// testutil.WithNetwork(network),
|
||
// )
|
||
func SetupProxyTest(ctx context.Context, backendCount int) (string, *BackendPool, error) {
|
||
// 使用共享网络名称
|
||
networkName := sharedNetworkName
|
||
|
||
// 尝试创建网络(如果不存在)
|
||
// 忽略"已存在"错误
|
||
_, err := CreateNetwork(ctx, networkName)
|
||
if err != nil && !isNetworkExistsError(err) {
|
||
return "", nil, fmt.Errorf("failed to create network: %w", err)
|
||
}
|
||
|
||
// 启动后端池并加入网络
|
||
pool, err := StartBackendPoolWithNetwork(ctx, backendCount, networkName)
|
||
if err != nil {
|
||
return "", nil, fmt.Errorf("failed to start backend pool: %w", err)
|
||
}
|
||
|
||
return networkName, pool, nil
|
||
}
|
||
|
||
// isNetworkExistsError 检查是否是网络已存在错误。
|
||
func isNetworkExistsError(err error) bool {
|
||
return err != nil && (containsString(err.Error(), "already exists") ||
|
||
containsString(err.Error(), "network with name") ||
|
||
containsString(err.Error(), "failed to create network"))
|
||
}
|
||
|
||
// containsString 检查字符串是否包含子串。
|
||
func containsString(s, substr string) bool {
|
||
return len(s) >= len(substr) && (s == substr || len(s) > 0 && containsSubstring(s, substr))
|
||
}
|
||
|
||
func containsSubstring(s, substr string) bool {
|
||
for i := 0; i <= len(s)-len(substr); i++ {
|
||
if s[i:i+len(substr)] == substr {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
// CleanupProxyTest 清理代理测试环境。
|
||
func CleanupProxyTest(ctx context.Context, networkName string, pool *BackendPool) {
|
||
if pool != nil {
|
||
pool.Terminate(ctx)
|
||
}
|
||
// 网络会随容器终止自动清理
|
||
}
|
||
|
||
// ProxyTestEnv 代理测试环境。
|
||
//
|
||
// 封装代理测试所需的资源。
|
||
type ProxyTestEnv struct {
|
||
Network string
|
||
Pool *BackendPool
|
||
Lolly *LollyContainer
|
||
Cleanup func()
|
||
HTTPClient *http.Client
|
||
}
|
||
|
||
// SetupProxyTestEnv 设置完整的代理测试环境。
|
||
//
|
||
// 创建网络、启动后端池、启动 lolly,返回封装的环境。
|
||
// 这是一个便捷函数,简化测试设置。
|
||
//
|
||
// 使用示例:
|
||
//
|
||
// env, err := testutil.SetupProxyTestEnv(ctx, t, 2, func(pool *testutil.BackendPool) string {
|
||
// cfg := testutil.NewConfigBuilder().
|
||
// WithServer(":8080").
|
||
// WithProxy("/", pool.InternalAddresses())
|
||
// yaml, _ := cfg.Build()
|
||
// return yaml
|
||
// })
|
||
func SetupProxyTestEnv(ctx context.Context, backendCount int, configBuilder func(*BackendPool) string) (*ProxyTestEnv, error) {
|
||
// 设置代理测试环境
|
||
networkName, pool, err := SetupProxyTest(ctx, backendCount)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// 构建配置
|
||
configYAML := configBuilder(pool)
|
||
|
||
// 启动 lolly
|
||
lolly, err := StartLolly(ctx,
|
||
WithConfigYAML(configYAML),
|
||
WithNetwork(networkName),
|
||
)
|
||
if err != nil {
|
||
CleanupProxyTest(ctx, networkName, pool)
|
||
return nil, fmt.Errorf("failed to start lolly: %w", err)
|
||
}
|
||
|
||
// 等待健康
|
||
if err := lolly.WaitForHealthy(ctx, 30*time.Second); err != nil {
|
||
CleanupProxyTest(ctx, networkName, pool)
|
||
lolly.Terminate(ctx)
|
||
return nil, fmt.Errorf("lolly not healthy: %w", err)
|
||
}
|
||
|
||
cleanup := func() {
|
||
lolly.Terminate(ctx)
|
||
CleanupProxyTest(ctx, networkName, pool)
|
||
}
|
||
|
||
return &ProxyTestEnv{
|
||
Network: networkName,
|
||
Pool: pool,
|
||
Lolly: lolly,
|
||
Cleanup: cleanup,
|
||
HTTPClient: &http.Client{Timeout: 10 * time.Second},
|
||
}, nil
|
||
}
|