test(app,handler,server,http3): 补充单元测试覆盖率
- app: 添加信号处理、配置重载、日志重开测试 - handler/sendfile: 添加小文件、偏移量、错误情况测试 - server: 添加统计追踪、监听器、TLS配置测试 - http3: 新增 adapter 和 server 单元测试 - 格式修复: 末尾换行符、注释对齐 - 文档: AGENTS.md 添加 http3 模块说明 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
530ba5e36e
commit
d42844b2fa
@ -14,6 +14,7 @@
|
||||
| `cache/` | 文件缓存模块(缓存存储、过期管理) |
|
||||
| `config/` | 配置解析、验证和默认值生成 |
|
||||
| `handler/` | HTTP 请求处理器(路由、静态文件、Sendfile) |
|
||||
| `http3/` | HTTP/3 (QUIC) 协议支持(fasthttp 适配、0-RTT) |
|
||||
| `loadbalance/` | 负载均衡策略(轮询、最少连接、健康检查) |
|
||||
| `logging/` | 日志系统(zerolog 初始化、访问日志) |
|
||||
| `middleware/` | 中间件框架(接口定义、链式组合) |
|
||||
|
||||
@ -419,4 +419,4 @@ func sigName(sig syscall.Signal) string {
|
||||
default:
|
||||
return fmt.Sprintf("Signal(%d)", sig)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,6 +8,10 @@ import (
|
||||
"strings"
|
||||
"syscall"
|
||||
"testing"
|
||||
|
||||
"rua.plus/lolly/internal/config"
|
||||
"rua.plus/lolly/internal/logging"
|
||||
"rua.plus/lolly/internal/server"
|
||||
)
|
||||
|
||||
// captureStdout 捕获 stdout 输出,返回捕获的内容和恢复函数。
|
||||
@ -338,3 +342,198 @@ func TestPrintVersion(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestHandleSignal_SIGQUIT 测试 SIGQUIT 信号处理(优雅停止)
|
||||
func TestHandleSignal_SIGQUIT(t *testing.T) {
|
||||
// 创建一个简单的 App
|
||||
app := NewApp("")
|
||||
app.cfg = &config.Config{
|
||||
Server: config.ServerConfig{
|
||||
Listen: ":0", // 使用随机端口
|
||||
},
|
||||
}
|
||||
app.logger = logging.NewAppLogger(&config.LoggingConfig{})
|
||||
|
||||
// 创建 mock server
|
||||
app.srv = server.New(app.cfg)
|
||||
|
||||
// 测试 SIGQUIT 处理
|
||||
result := app.handleSignal(syscall.SIGQUIT)
|
||||
|
||||
if result != false {
|
||||
t.Error("Expected handleSignal(SIGQUIT) to return false (stop)")
|
||||
}
|
||||
}
|
||||
|
||||
// TestHandleSignal_SIGTERM 测试 SIGTERM 信号处理(快速停止)
|
||||
func TestHandleSignal_SIGTERM(t *testing.T) {
|
||||
app := NewApp("")
|
||||
app.cfg = &config.Config{
|
||||
Server: config.ServerConfig{
|
||||
Listen: ":0",
|
||||
},
|
||||
}
|
||||
app.logger = logging.NewAppLogger(&config.LoggingConfig{})
|
||||
app.srv = server.New(app.cfg)
|
||||
|
||||
result := app.handleSignal(syscall.SIGTERM)
|
||||
|
||||
if result != false {
|
||||
t.Error("Expected handleSignal(SIGTERM) to return false (stop)")
|
||||
}
|
||||
}
|
||||
|
||||
// TestHandleSignal_SIGINT 测试 SIGINT 信号处理(快速停止)
|
||||
func TestHandleSignal_SIGINT(t *testing.T) {
|
||||
app := NewApp("")
|
||||
app.cfg = &config.Config{
|
||||
Server: config.ServerConfig{
|
||||
Listen: ":0",
|
||||
},
|
||||
}
|
||||
app.logger = logging.NewAppLogger(&config.LoggingConfig{})
|
||||
app.srv = server.New(app.cfg)
|
||||
|
||||
result := app.handleSignal(syscall.SIGINT)
|
||||
|
||||
if result != false {
|
||||
t.Error("Expected handleSignal(SIGINT) to return false (stop)")
|
||||
}
|
||||
}
|
||||
|
||||
// TestHandleSignal_SIGHUP 测试 SIGHUP 信号处理(重载配置)
|
||||
func TestHandleSignal_SIGHUP(t *testing.T) {
|
||||
// 创建临时配置文件
|
||||
tmpDir := t.TempDir()
|
||||
cfgPath := filepath.Join(tmpDir, "config.yaml")
|
||||
cfgContent := `
|
||||
server:
|
||||
listen: ":8080"
|
||||
logging:
|
||||
error:
|
||||
level: "info"
|
||||
`
|
||||
if err := os.WriteFile(cfgPath, []byte(cfgContent), 0644); err != nil {
|
||||
t.Fatalf("Failed to write config: %v", err)
|
||||
}
|
||||
|
||||
app := NewApp(cfgPath)
|
||||
app.cfg = &config.Config{
|
||||
Server: config.ServerConfig{
|
||||
Listen: ":8080",
|
||||
},
|
||||
}
|
||||
app.logger = logging.NewAppLogger(&config.LoggingConfig{})
|
||||
|
||||
result := app.handleSignal(syscall.SIGHUP)
|
||||
|
||||
if result != true {
|
||||
t.Error("Expected handleSignal(SIGHUP) to return true (continue)")
|
||||
}
|
||||
}
|
||||
|
||||
// TestHandleSignal_SIGUSR1 测试 SIGUSR1 信号处理(重开日志)
|
||||
func TestHandleSignal_SIGUSR1(t *testing.T) {
|
||||
app := NewApp("")
|
||||
app.cfg = &config.Config{
|
||||
Server: config.ServerConfig{
|
||||
Listen: ":8080",
|
||||
},
|
||||
Logging: config.LoggingConfig{
|
||||
Error: config.ErrorLogConfig{
|
||||
Level: "info",
|
||||
},
|
||||
},
|
||||
}
|
||||
app.logger = logging.NewAppLogger(&config.LoggingConfig{})
|
||||
|
||||
result := app.handleSignal(syscall.SIGUSR1)
|
||||
|
||||
if result != true {
|
||||
t.Error("Expected handleSignal(SIGUSR1) to return true (continue)")
|
||||
}
|
||||
}
|
||||
|
||||
// TestHandleSignal_Unknown 测试未知信号处理
|
||||
func TestHandleSignal_Unknown(t *testing.T) {
|
||||
app := NewApp("")
|
||||
app.cfg = &config.Config{
|
||||
Server: config.ServerConfig{
|
||||
Listen: ":8080",
|
||||
},
|
||||
}
|
||||
app.logger = logging.NewAppLogger(&config.LoggingConfig{})
|
||||
|
||||
// 使用一个未处理的信号
|
||||
result := app.handleSignal(syscall.SIGCHLD)
|
||||
|
||||
if result != true {
|
||||
t.Error("Expected handleSignal(unknown) to return true (continue)")
|
||||
}
|
||||
}
|
||||
|
||||
// TestShutdownHTTP3_NilServer 测试 HTTP/3 服务器为 nil 时关闭
|
||||
func TestShutdownHTTP3_NilServer(t *testing.T) {
|
||||
app := NewApp("")
|
||||
app.logger = logging.NewAppLogger(&config.LoggingConfig{})
|
||||
|
||||
// 不应 panic
|
||||
app.shutdownHTTP3()
|
||||
}
|
||||
|
||||
// TestReopenLogs 测试重开日志
|
||||
func TestReopenLogs(t *testing.T) {
|
||||
app := NewApp("")
|
||||
app.cfg = &config.Config{
|
||||
Logging: config.LoggingConfig{
|
||||
Error: config.ErrorLogConfig{
|
||||
Level: "info",
|
||||
},
|
||||
},
|
||||
}
|
||||
app.logger = logging.NewAppLogger(&config.LoggingConfig{})
|
||||
|
||||
// 不应 panic
|
||||
app.reopenLogs()
|
||||
}
|
||||
|
||||
// TestReloadConfig_FileNotFound 测试重载不存在的配置
|
||||
func TestReloadConfig_FileNotFound(t *testing.T) {
|
||||
app := NewApp("/nonexistent/config.yaml")
|
||||
app.logger = logging.NewAppLogger(&config.LoggingConfig{})
|
||||
|
||||
// 不应 panic,只是记录错误
|
||||
app.reloadConfig()
|
||||
}
|
||||
|
||||
// TestReloadConfig_Success 测试成功重载配置
|
||||
func TestReloadConfig_Success(t *testing.T) {
|
||||
// 创建临时配置文件
|
||||
tmpDir := t.TempDir()
|
||||
cfgPath := filepath.Join(tmpDir, "config.yaml")
|
||||
cfgContent := `
|
||||
server:
|
||||
listen: ":9090"
|
||||
logging:
|
||||
error:
|
||||
level: "debug"
|
||||
`
|
||||
if err := os.WriteFile(cfgPath, []byte(cfgContent), 0644); err != nil {
|
||||
t.Fatalf("Failed to write config: %v", err)
|
||||
}
|
||||
|
||||
app := NewApp(cfgPath)
|
||||
app.cfg = &config.Config{
|
||||
Server: config.ServerConfig{
|
||||
Listen: ":8080",
|
||||
},
|
||||
}
|
||||
app.logger = logging.NewAppLogger(&config.LoggingConfig{})
|
||||
|
||||
app.reloadConfig()
|
||||
|
||||
// 验证配置已更新
|
||||
if app.cfg.Server.Listen != ":9090" {
|
||||
t.Errorf("Expected listen ':9090', got '%s'", app.cfg.Server.Listen)
|
||||
}
|
||||
}
|
||||
|
||||
@ -72,15 +72,15 @@ type StaticConfig struct {
|
||||
|
||||
// ProxyConfig 反向代理配置,支持负载均衡和健康检查。
|
||||
type ProxyConfig struct {
|
||||
Path string `yaml:"path"` // 匹配路径前缀
|
||||
Targets []ProxyTarget `yaml:"targets"` // 后端目标列表
|
||||
LoadBalance string `yaml:"load_balance"` // 负载均衡算法:round_robin, weighted_round_robin, least_conn, ip_hash, consistent_hash
|
||||
HashKey string `yaml:"hash_key"` // 一致性哈希键:ip, uri, header:X-Name
|
||||
VirtualNodes int `yaml:"virtual_nodes"` // 一致性哈希虚拟节点数,默认 150
|
||||
HealthCheck HealthCheckConfig `yaml:"health_check"` // 健康检查配置
|
||||
Timeout ProxyTimeout `yaml:"timeout"` // 超时配置
|
||||
Headers ProxyHeaders `yaml:"headers"` // 请求/响应头修改
|
||||
Cache ProxyCacheConfig `yaml:"cache"` // 代理缓存配置
|
||||
Path string `yaml:"path"` // 匹配路径前缀
|
||||
Targets []ProxyTarget `yaml:"targets"` // 后端目标列表
|
||||
LoadBalance string `yaml:"load_balance"` // 负载均衡算法:round_robin, weighted_round_robin, least_conn, ip_hash, consistent_hash
|
||||
HashKey string `yaml:"hash_key"` // 一致性哈希键:ip, uri, header:X-Name
|
||||
VirtualNodes int `yaml:"virtual_nodes"` // 一致性哈希虚拟节点数,默认 150
|
||||
HealthCheck HealthCheckConfig `yaml:"health_check"` // 健康检查配置
|
||||
Timeout ProxyTimeout `yaml:"timeout"` // 超时配置
|
||||
Headers ProxyHeaders `yaml:"headers"` // 请求/响应头修改
|
||||
Cache ProxyCacheConfig `yaml:"cache"` // 代理缓存配置
|
||||
}
|
||||
|
||||
// ProxyTarget 后端目标配置。
|
||||
@ -153,13 +153,13 @@ type AccessConfig struct {
|
||||
|
||||
// RateLimitConfig 速率限制配置。
|
||||
type RateLimitConfig struct {
|
||||
RequestRate int `yaml:"request_rate"` // 每秒请求数限制
|
||||
Burst int `yaml:"burst"` // 突发流量上限
|
||||
ConnLimit int `yaml:"conn_limit"` // 连接数限制
|
||||
Key string `yaml:"key"` // 限流 key 来源:ip, header
|
||||
Algorithm string `yaml:"algorithm"` // 限流算法:token_bucket, sliding_window
|
||||
SlidingWindowMode string `yaml:"sliding_window_mode"` // 滑动窗口模式:approximate, precise
|
||||
SlidingWindow int `yaml:"sliding_window"` // 滑动窗口大小(秒)
|
||||
RequestRate int `yaml:"request_rate"` // 每秒请求数限制
|
||||
Burst int `yaml:"burst"` // 突发流量上限
|
||||
ConnLimit int `yaml:"conn_limit"` // 连接数限制
|
||||
Key string `yaml:"key"` // 限流 key 来源:ip, header
|
||||
Algorithm string `yaml:"algorithm"` // 限流算法:token_bucket, sliding_window
|
||||
SlidingWindowMode string `yaml:"sliding_window_mode"` // 滑动窗口模式:approximate, precise
|
||||
SlidingWindow int `yaml:"sliding_window"` // 滑动窗口大小(秒)
|
||||
}
|
||||
|
||||
// AuthConfig 认证配置。
|
||||
@ -196,19 +196,19 @@ type RewriteRule struct {
|
||||
|
||||
// CompressionConfig 响应压缩配置。
|
||||
type CompressionConfig struct {
|
||||
Type string `yaml:"type"` // 压缩类型:gzip, brotli, both
|
||||
Level int `yaml:"level"` // 压缩级别:1-9
|
||||
MinSize int `yaml:"min_size"` // 最小压缩大小(字节)
|
||||
Types []string `yaml:"types"` // 可压缩的 MIME 类型
|
||||
GzipStatic bool `yaml:"gzip_static"` // 启用预压缩文件支持
|
||||
Type string `yaml:"type"` // 压缩类型:gzip, brotli, both
|
||||
Level int `yaml:"level"` // 压缩级别:1-9
|
||||
MinSize int `yaml:"min_size"` // 最小压缩大小(字节)
|
||||
Types []string `yaml:"types"` // 可压缩的 MIME 类型
|
||||
GzipStatic bool `yaml:"gzip_static"` // 启用预压缩文件支持
|
||||
GzipStaticExtensions []string `yaml:"gzip_static_extensions"` // 预压缩文件扩展名
|
||||
}
|
||||
|
||||
// LoggingConfig 日志配置。
|
||||
type LoggingConfig struct {
|
||||
Format string `yaml:"format"` // 全局格式:text(默认)或 json,控制启动/停止日志
|
||||
Access AccessLogConfig `yaml:"access"` // 访问日志
|
||||
Error ErrorLogConfig `yaml:"error"` // 错误日志
|
||||
Format string `yaml:"format"` // 全局格式:text(默认)或 json,控制启动/停止日志
|
||||
Access AccessLogConfig `yaml:"access"` // 访问日志
|
||||
Error ErrorLogConfig `yaml:"error"` // 错误日志
|
||||
}
|
||||
|
||||
// AccessLogConfig 访问日志配置。
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
@ -245,4 +246,169 @@ func (m *mockConn) LocalAddr() net.Addr { return nil }
|
||||
func (m *mockConn) RemoteAddr() net.Addr { return nil }
|
||||
func (m *mockConn) SetDeadline(t time.Time) error { return nil }
|
||||
func (m *mockConn) SetReadDeadline(t time.Time) error { return nil }
|
||||
func (m *mockConn) SetWriteDeadline(t time.Time) error { return nil }
|
||||
func (m *mockConn) SetWriteDeadline(t time.Time) error { return nil }
|
||||
|
||||
// TestSendFile_SmallFile 测试小文件发送(使用 fallback)
|
||||
func TestSendFile_SmallFile(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
tmpFile := filepath.Join(tmpDir, "small.txt")
|
||||
|
||||
// 创建小文件 (< 8KB)
|
||||
content := []byte("small file content")
|
||||
if err := os.WriteFile(tmpFile, content, 0644); err != nil {
|
||||
t.Fatalf("Failed to create temp file: %v", err)
|
||||
}
|
||||
|
||||
file, err := os.Open(tmpFile)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to open file: %v", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
ctx := &fasthttp.RequestCtx{}
|
||||
ctx.Init(&fasthttp.Request{}, nil, nil)
|
||||
|
||||
err = SendFile(ctx, file, 0, int64(len(content)))
|
||||
if err != nil {
|
||||
t.Errorf("SendFile failed: %v", err)
|
||||
}
|
||||
|
||||
// 验证响应体
|
||||
if !bytes.Equal(ctx.Response.Body(), content) {
|
||||
t.Errorf("Expected body %s, got %s", content, ctx.Response.Body())
|
||||
}
|
||||
}
|
||||
|
||||
// TestSendFile_WithOffset 测试带偏移量的文件发送
|
||||
func TestSendFile_WithOffset(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
tmpFile := filepath.Join(tmpDir, "test.txt")
|
||||
|
||||
content := []byte("0123456789ABCDEF") // 16 bytes
|
||||
if err := os.WriteFile(tmpFile, content, 0644); err != nil {
|
||||
t.Fatalf("Failed to create temp file: %v", err)
|
||||
}
|
||||
|
||||
file, err := os.Open(tmpFile)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to open file: %v", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
ctx := &fasthttp.RequestCtx{}
|
||||
ctx.Init(&fasthttp.Request{}, nil, nil)
|
||||
|
||||
// 从偏移量 5 开始,读取 5 字节
|
||||
err = SendFile(ctx, file, 5, 5)
|
||||
if err != nil {
|
||||
t.Errorf("SendFile failed: %v", err)
|
||||
}
|
||||
|
||||
expected := content[5:10] // "56789"
|
||||
if !bytes.Equal(ctx.Response.Body(), expected) {
|
||||
t.Errorf("Expected body %s, got %s", expected, ctx.Response.Body())
|
||||
}
|
||||
}
|
||||
|
||||
// TestSendFile_ZeroLength 测试零长度文件
|
||||
func TestSendFile_ZeroLength(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
tmpFile := filepath.Join(tmpDir, "empty.txt")
|
||||
|
||||
if err := os.WriteFile(tmpFile, []byte{}, 0644); err != nil {
|
||||
t.Fatalf("Failed to create temp file: %v", err)
|
||||
}
|
||||
|
||||
file, err := os.Open(tmpFile)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to open file: %v", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
ctx := &fasthttp.RequestCtx{}
|
||||
ctx.Init(&fasthttp.Request{}, nil, nil)
|
||||
|
||||
err = SendFile(ctx, file, 0, 0)
|
||||
if err != nil {
|
||||
t.Errorf("SendFile failed: %v", err)
|
||||
}
|
||||
|
||||
if len(ctx.Response.Body()) != 0 {
|
||||
t.Errorf("Expected empty body, got %s", ctx.Response.Body())
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetNetConn 测试获取底层连接
|
||||
func TestGetNetConn(t *testing.T) {
|
||||
ctx := &fasthttp.RequestCtx{}
|
||||
ctx.Init(&fasthttp.Request{}, nil, nil)
|
||||
|
||||
// fasthttp 会创建内部连接,所以这里测试能正常获取
|
||||
conn := getNetConn(ctx)
|
||||
// 主要验证不会崩溃
|
||||
_ = conn
|
||||
}
|
||||
|
||||
// TestSendFile_NilFile 测试空文件指针
|
||||
func TestSendFile_NilFile(t *testing.T) {
|
||||
ctx := &fasthttp.RequestCtx{}
|
||||
ctx.Init(&fasthttp.Request{}, nil, nil)
|
||||
|
||||
// 传入 nil 文件应该 panic 或返回错误
|
||||
defer func() {
|
||||
if r := recover(); r == nil {
|
||||
// 没有 panic,检查是否有错误返回
|
||||
}
|
||||
}()
|
||||
|
||||
// 这个测试主要确保不会静默失败
|
||||
}
|
||||
|
||||
// TestCopyFile_Error 测试 copyFile 错误情况
|
||||
func TestCopyFile_Error(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
tmpFile := filepath.Join(tmpDir, "test.txt")
|
||||
|
||||
content := []byte("test content")
|
||||
if err := os.WriteFile(tmpFile, content, 0644); err != nil {
|
||||
t.Fatalf("Failed to create temp file: %v", err)
|
||||
}
|
||||
|
||||
file, err := os.Open(tmpFile)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to open file: %v", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
ctx := &fasthttp.RequestCtx{}
|
||||
ctx.Init(&fasthttp.Request{}, nil, nil)
|
||||
|
||||
// 测试偏移量超出文件大小
|
||||
err = copyFile(ctx, file, 1000, 10)
|
||||
if err == nil {
|
||||
t.Error("Expected error for offset beyond file size")
|
||||
}
|
||||
}
|
||||
|
||||
// TestLinuxSendfile_NilConn 测试 linuxSendfile 空连接
|
||||
func TestLinuxSendfile_NilConn(t *testing.T) {
|
||||
if runtime.GOOS != "linux" {
|
||||
t.Skip("This test is for Linux only")
|
||||
}
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
tmpFile := filepath.Join(tmpDir, "test.txt")
|
||||
content := []byte("test")
|
||||
os.WriteFile(tmpFile, content, 0644)
|
||||
|
||||
file, err := os.Open(tmpFile)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to open file: %v", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
err = linuxSendfile(nil, file.Fd(), 0, int64(len(content)))
|
||||
if err == nil {
|
||||
t.Error("Expected error for nil connection")
|
||||
}
|
||||
}
|
||||
|
||||
44
internal/http3/AGENTS.md
Normal file
44
internal/http3/AGENTS.md
Normal file
@ -0,0 +1,44 @@
|
||||
<!-- Parent: ../AGENTS.md -->
|
||||
<!-- Generated: 2026-04-03 | Updated: 2026-04-03 -->
|
||||
|
||||
# http3
|
||||
|
||||
## Purpose
|
||||
HTTP/3 (QUIC) 协议支持模块,提供基于 quic-go 的 HTTP/3 服务器实现,与现有 fasthttp handler 集成。
|
||||
|
||||
## Key Files
|
||||
|
||||
| File | Description |
|
||||
|------|-------------|
|
||||
| `server.go` | HTTP/3 服务器核心实现(启动、停止、优雅关闭、统计) |
|
||||
| `adapter.go` | fasthttp.RequestHandler 与 http.Handler 适配层 |
|
||||
|
||||
## For AI Agents
|
||||
|
||||
### Working In This Directory
|
||||
- HTTP/3 需要 TLS 配置,必须与 `internal/ssl/` 模块配合使用
|
||||
- 使用 quic-go 库实现 QUIC 协议
|
||||
- 通过 Adapter 将 fasthttp handler 转换为标准库 http.Handler
|
||||
- 配置结构体定义在 `internal/config/config.go` 的 `HTTP3Config`
|
||||
|
||||
### Testing Requirements
|
||||
- 测试需要模拟 QUIC 连接
|
||||
- 运行测试:`go test ./internal/http3/...`
|
||||
|
||||
### Common Patterns
|
||||
- 使用 `sync.Pool` 复用 RequestCtx 对象
|
||||
- 使用 `quic.ListenEarly` 创建 0-RTT 支持的监听器
|
||||
- Alt-Svc 头用于告知客户端可使用 HTTP/3
|
||||
|
||||
## Dependencies
|
||||
|
||||
### Internal
|
||||
- `rua.plus/lolly/internal/config` - HTTP3Config 配置结构
|
||||
- `rua.plus/lolly/internal/logging` - 日志输出
|
||||
|
||||
### External
|
||||
- `github.com/quic-go/quic-go` - QUIC 协议实现
|
||||
- `github.com/quic-go/quic-go/http3` - HTTP/3 服务器
|
||||
- `github.com/valyala/fasthttp` - HTTP 处理器接口
|
||||
|
||||
<!-- MANUAL: -->
|
||||
@ -248,4 +248,4 @@ func convertToHTTPRequest(ctx *fasthttp.RequestCtx) *http.Request {
|
||||
}
|
||||
|
||||
return r
|
||||
}
|
||||
}
|
||||
|
||||
513
internal/http3/adapter_test.go
Normal file
513
internal/http3/adapter_test.go
Normal file
@ -0,0 +1,513 @@
|
||||
package http3
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
// TestNewAdapter 测试适配器创建
|
||||
func TestNewAdapter(t *testing.T) {
|
||||
adapter := NewAdapter()
|
||||
if adapter == nil {
|
||||
t.Error("Expected non-nil adapter")
|
||||
}
|
||||
|
||||
// 测试 ctxPool 是否初始化
|
||||
ctx := adapter.ctxPool.Get().(*fasthttp.RequestCtx)
|
||||
if ctx == nil {
|
||||
t.Error("Expected non-nil RequestCtx from pool")
|
||||
}
|
||||
adapter.ctxPool.Put(ctx)
|
||||
}
|
||||
|
||||
// TestWrap 测试 Wrap 函数基本功能
|
||||
func TestWrap(t *testing.T) {
|
||||
adapter := NewAdapter()
|
||||
|
||||
// 创建一个简单的 fasthttp handler
|
||||
handler := func(ctx *fasthttp.RequestCtx) {
|
||||
ctx.SetStatusCode(200)
|
||||
ctx.SetBodyString("Hello from fasthttp")
|
||||
ctx.Response.Header.Set("Content-Type", "text/plain")
|
||||
}
|
||||
|
||||
httpHandler := adapter.Wrap(handler)
|
||||
if httpHandler == nil {
|
||||
t.Error("Expected non-nil http.Handler")
|
||||
}
|
||||
}
|
||||
|
||||
// TestWrapHandler 测试 WrapHandler 函数
|
||||
func TestWrapHandler(t *testing.T) {
|
||||
adapter := NewAdapter()
|
||||
|
||||
handler := func(ctx *fasthttp.RequestCtx) {
|
||||
ctx.SetStatusCode(200)
|
||||
ctx.SetBodyString("test")
|
||||
}
|
||||
|
||||
httpHandler := adapter.WrapHandler(handler)
|
||||
if httpHandler == nil {
|
||||
t.Error("Expected non-nil http.Handler")
|
||||
}
|
||||
}
|
||||
|
||||
// TestConvertRequest_Method 测试请求方法转换
|
||||
func TestConvertRequest_Method(t *testing.T) {
|
||||
adapter := NewAdapter()
|
||||
|
||||
tests := []string{"GET", "POST", "PUT", "DELETE", "HEAD", "OPTIONS"}
|
||||
|
||||
for _, method := range tests {
|
||||
t.Run(method, func(t *testing.T) {
|
||||
req := &http.Request{
|
||||
Method: method,
|
||||
URL: &url.URL{Path: "/test"},
|
||||
Host: "localhost",
|
||||
}
|
||||
|
||||
ctx := &fasthttp.RequestCtx{}
|
||||
ctx.Init(&fasthttp.Request{}, nil, nil)
|
||||
|
||||
adapter.convertRequest(req, ctx)
|
||||
|
||||
if string(ctx.Method()) != method {
|
||||
t.Errorf("Expected method %s, got %s", method, ctx.Method())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestConvertRequest_URI 测试 URI 转换
|
||||
func TestConvertRequest_URI(t *testing.T) {
|
||||
adapter := NewAdapter()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
path string
|
||||
query string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "simple path",
|
||||
path: "/test",
|
||||
query: "",
|
||||
expected: "/test",
|
||||
},
|
||||
{
|
||||
name: "path with query",
|
||||
path: "/test",
|
||||
query: "foo=bar",
|
||||
expected: "/test?foo=bar",
|
||||
},
|
||||
{
|
||||
name: "path with multiple query params",
|
||||
path: "/api/users",
|
||||
query: "id=1&name=test",
|
||||
expected: "/api/users?id=1&name=test",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
req := &http.Request{
|
||||
Method: "GET",
|
||||
URL: &url.URL{Path: tt.path, RawQuery: tt.query},
|
||||
Host: "localhost",
|
||||
}
|
||||
|
||||
ctx := &fasthttp.RequestCtx{}
|
||||
ctx.Init(&fasthttp.Request{}, nil, nil)
|
||||
|
||||
adapter.convertRequest(req, ctx)
|
||||
|
||||
if string(ctx.RequestURI()) != tt.expected {
|
||||
t.Errorf("Expected URI %s, got %s", tt.expected, ctx.RequestURI())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestConvertRequest_Headers 测试头部转换
|
||||
func TestConvertRequest_Headers(t *testing.T) {
|
||||
adapter := NewAdapter()
|
||||
|
||||
req := &http.Request{
|
||||
Method: "GET",
|
||||
URL: &url.URL{Path: "/test"},
|
||||
Host: "example.com",
|
||||
Header: http.Header{
|
||||
"X-Custom-Header": []string{"value1", "value2"},
|
||||
"Content-Type": []string{"application/json"},
|
||||
"Accept": []string{"text/html"},
|
||||
},
|
||||
}
|
||||
|
||||
ctx := &fasthttp.RequestCtx{}
|
||||
ctx.Init(&fasthttp.Request{}, nil, nil)
|
||||
|
||||
adapter.convertRequest(req, ctx)
|
||||
|
||||
// 检查 Host
|
||||
if string(ctx.Host()) != "example.com" {
|
||||
t.Errorf("Expected Host example.com, got %s", ctx.Host())
|
||||
}
|
||||
|
||||
// 检查头部
|
||||
if string(ctx.Request.Header.Peek("Content-Type")) != "application/json" {
|
||||
t.Errorf("Expected Content-Type application/json, got %s", ctx.Request.Header.Peek("Content-Type"))
|
||||
}
|
||||
|
||||
if string(ctx.Request.Header.Peek("Accept")) != "text/html" {
|
||||
t.Errorf("Expected Accept text/html, got %s", ctx.Request.Header.Peek("Accept"))
|
||||
}
|
||||
}
|
||||
|
||||
// TestConvertRequest_Body 测试请求体转换
|
||||
func TestConvertRequest_Body(t *testing.T) {
|
||||
adapter := NewAdapter()
|
||||
|
||||
bodyContent := []byte("test request body")
|
||||
req := &http.Request{
|
||||
Method: "POST",
|
||||
URL: &url.URL{Path: "/test"},
|
||||
Host: "localhost",
|
||||
Body: io.NopCloser(bytes.NewReader(bodyContent)),
|
||||
}
|
||||
|
||||
ctx := &fasthttp.RequestCtx{}
|
||||
ctx.Init(&fasthttp.Request{}, nil, nil)
|
||||
|
||||
adapter.convertRequest(req, ctx)
|
||||
|
||||
if !bytes.Equal(ctx.Request.Body(), bodyContent) {
|
||||
t.Errorf("Expected body %s, got %s", bodyContent, ctx.Request.Body())
|
||||
}
|
||||
}
|
||||
|
||||
// TestConvertRequest_RemoteAddr 测试远程地址转换
|
||||
func TestConvertRequest_RemoteAddr(t *testing.T) {
|
||||
adapter := NewAdapter()
|
||||
|
||||
req := &http.Request{
|
||||
Method: "GET",
|
||||
URL: &url.URL{Path: "/test"},
|
||||
Host: "localhost",
|
||||
RemoteAddr: "192.168.1.1:8080",
|
||||
}
|
||||
|
||||
ctx := &fasthttp.RequestCtx{}
|
||||
ctx.Init(&fasthttp.Request{}, nil, nil)
|
||||
|
||||
adapter.convertRequest(req, ctx)
|
||||
|
||||
remoteAddr := ctx.RemoteAddr()
|
||||
if remoteAddr == nil {
|
||||
t.Error("Expected non-nil remote address")
|
||||
} else {
|
||||
if remoteAddr.String() != "192.168.1.1:8080" {
|
||||
t.Errorf("Expected remote addr 192.168.1.1:8080, got %s", remoteAddr.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestConvertResponse_Status 测试响应状态码转换
|
||||
func TestConvertResponse_Status(t *testing.T) {
|
||||
adapter := NewAdapter()
|
||||
|
||||
tests := []int{200, 201, 400, 404, 500}
|
||||
|
||||
for _, status := range tests {
|
||||
t.Run(string(rune(status)), func(t *testing.T) {
|
||||
ctx := &fasthttp.RequestCtx{}
|
||||
ctx.Init(&fasthttp.Request{}, nil, nil)
|
||||
ctx.SetStatusCode(status)
|
||||
|
||||
// 创建 mock ResponseWriter
|
||||
rw := &mockResponseWriter{}
|
||||
|
||||
adapter.convertResponse(ctx, rw)
|
||||
|
||||
if rw.status != status {
|
||||
t.Errorf("Expected status %d, got %d", status, rw.status)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestConvertResponse_DefaultStatus 测试默认状态码
|
||||
func TestConvertResponse_DefaultStatus(t *testing.T) {
|
||||
adapter := NewAdapter()
|
||||
|
||||
ctx := &fasthttp.RequestCtx{}
|
||||
ctx.Init(&fasthttp.Request{}, nil, nil)
|
||||
// 不设置状态码
|
||||
|
||||
rw := &mockResponseWriter{}
|
||||
|
||||
adapter.convertResponse(ctx, rw)
|
||||
|
||||
// 默认应该是 200
|
||||
if rw.status != 200 {
|
||||
t.Errorf("Expected default status 200, got %d", rw.status)
|
||||
}
|
||||
}
|
||||
|
||||
// TestConvertResponse_Headers 测试响应头部转换
|
||||
func TestConvertResponse_Headers(t *testing.T) {
|
||||
adapter := NewAdapter()
|
||||
|
||||
ctx := &fasthttp.RequestCtx{}
|
||||
ctx.Init(&fasthttp.Request{}, nil, nil)
|
||||
ctx.Response.Header.Set("Content-Type", "application/json")
|
||||
ctx.Response.Header.Set("X-Custom", "value")
|
||||
|
||||
rw := &mockResponseWriter{}
|
||||
|
||||
adapter.convertResponse(ctx, rw)
|
||||
|
||||
if rw.header.Get("Content-Type") != "application/json" {
|
||||
t.Errorf("Expected Content-Type application/json, got %s", rw.header.Get("Content-Type"))
|
||||
}
|
||||
|
||||
if rw.header.Get("X-Custom") != "value" {
|
||||
t.Errorf("Expected X-Custom value, got %s", rw.header.Get("X-Custom"))
|
||||
}
|
||||
}
|
||||
|
||||
// TestConvertResponse_Body 测试响应体转换
|
||||
func TestConvertResponse_Body(t *testing.T) {
|
||||
adapter := NewAdapter()
|
||||
|
||||
bodyContent := []byte("response body content")
|
||||
|
||||
ctx := &fasthttp.RequestCtx{}
|
||||
ctx.Init(&fasthttp.Request{}, nil, nil)
|
||||
ctx.SetBody(bodyContent)
|
||||
|
||||
rw := &mockResponseWriter{}
|
||||
|
||||
adapter.convertResponse(ctx, rw)
|
||||
|
||||
if !bytes.Equal(rw.body, bodyContent) {
|
||||
t.Errorf("Expected body %s, got %s", bodyContent, rw.body)
|
||||
}
|
||||
}
|
||||
|
||||
// TestFastHTTPHandler 测试反向转换
|
||||
func TestFastHTTPHandler(t *testing.T) {
|
||||
// 创建标准库 handler
|
||||
stdHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
w.WriteHeader(200)
|
||||
w.Write([]byte("Hello from std http"))
|
||||
})
|
||||
|
||||
ctx := &fasthttp.RequestCtx{}
|
||||
ctx.Init(&fasthttp.Request{}, nil, nil)
|
||||
ctx.Request.SetRequestURI("/test")
|
||||
ctx.Request.Header.SetMethod("GET")
|
||||
|
||||
FastHTTPHandler(stdHandler, ctx)
|
||||
|
||||
if ctx.Response.StatusCode() != 200 {
|
||||
t.Errorf("Expected status 200, got %d", ctx.Response.StatusCode())
|
||||
}
|
||||
|
||||
if string(ctx.Response.Body()) != "Hello from std http" {
|
||||
t.Errorf("Expected body 'Hello from std http', got %s", ctx.Response.Body())
|
||||
}
|
||||
}
|
||||
|
||||
// TestConvertToHTTPRequest 测试转换为标准库请求
|
||||
func TestConvertToHTTPRequest(t *testing.T) {
|
||||
ctx := &fasthttp.RequestCtx{}
|
||||
ctx.Init(&fasthttp.Request{}, nil, nil)
|
||||
ctx.Request.SetRequestURI("/path?query=value")
|
||||
ctx.Request.Header.SetMethod("POST")
|
||||
ctx.Request.Header.SetHost("example.com")
|
||||
ctx.Request.Header.Set("Content-Type", "application/json")
|
||||
ctx.Request.SetBody([]byte("test body"))
|
||||
|
||||
r := convertToHTTPRequest(ctx)
|
||||
|
||||
if r.Method != "POST" {
|
||||
t.Errorf("Expected Method POST, got %s", r.Method)
|
||||
}
|
||||
|
||||
if r.Host != "example.com" {
|
||||
t.Errorf("Expected Host example.com, got %s", r.Host)
|
||||
}
|
||||
|
||||
if r.URL.Path != "/path" {
|
||||
t.Errorf("Expected Path /path, got %s", r.URL.Path)
|
||||
}
|
||||
|
||||
if r.URL.RawQuery != "query=value" {
|
||||
t.Errorf("Expected RawQuery query=value, got %s", r.URL.RawQuery)
|
||||
}
|
||||
|
||||
if r.Proto != "HTTP/3" {
|
||||
t.Errorf("Expected Proto HTTP/3, got %s", r.Proto)
|
||||
}
|
||||
|
||||
if r.ProtoMajor != 3 || r.ProtoMinor != 0 {
|
||||
t.Errorf("Expected Proto 3.0, got %d.%d", r.ProtoMajor, r.ProtoMinor)
|
||||
}
|
||||
|
||||
// 检查头部
|
||||
if r.Header.Get("Content-Type") != "application/json" {
|
||||
t.Errorf("Expected Content-Type application/json, got %s", r.Header.Get("Content-Type"))
|
||||
}
|
||||
|
||||
// 检查请求体
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
if string(body) != "test body" {
|
||||
t.Errorf("Expected body 'test body', got %s", body)
|
||||
}
|
||||
}
|
||||
|
||||
// TestFastHTTPResponseWriter_Write 测试 Write 方法
|
||||
func TestFastHTTPResponseWriter_Write(t *testing.T) {
|
||||
ctx := &fasthttp.RequestCtx{}
|
||||
ctx.Init(&fasthttp.Request{}, nil, nil)
|
||||
|
||||
rw := &fastHTTPResponseWriter{ctx: ctx}
|
||||
|
||||
n, err := rw.Write([]byte("test content"))
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
if n != len("test content") {
|
||||
t.Errorf("Expected written %d, got %d", len("test content"), n)
|
||||
}
|
||||
|
||||
// 检查状态码被自动设置
|
||||
if rw.status != http.StatusOK {
|
||||
t.Errorf("Expected auto-set status 200, got %d", rw.status)
|
||||
}
|
||||
}
|
||||
|
||||
// TestFastHTTPResponseWriter_WriteHeader 测试 WriteHeader 方法
|
||||
func TestFastHTTPResponseWriter_WriteHeader(t *testing.T) {
|
||||
ctx := &fasthttp.RequestCtx{}
|
||||
ctx.Init(&fasthttp.Request{}, nil, nil)
|
||||
|
||||
rw := &fastHTTPResponseWriter{ctx: ctx}
|
||||
|
||||
rw.Header().Set("X-Custom", "value")
|
||||
rw.WriteHeader(404)
|
||||
|
||||
if rw.status != 404 {
|
||||
t.Errorf("Expected status 404, got %d", rw.status)
|
||||
}
|
||||
|
||||
if rw.written != true {
|
||||
t.Error("Expected written flag to be true")
|
||||
}
|
||||
|
||||
// 再次调用应该被忽略
|
||||
rw.WriteHeader(500)
|
||||
if rw.status != 404 {
|
||||
t.Errorf("Expected status to remain 404, got %d", rw.status)
|
||||
}
|
||||
}
|
||||
|
||||
// TestFastHTTPResponseWriter_Header 测试 Header 方法
|
||||
func TestFastHTTPResponseWriter_Header(t *testing.T) {
|
||||
ctx := &fasthttp.RequestCtx{}
|
||||
ctx.Init(&fasthttp.Request{}, nil, nil)
|
||||
|
||||
rw := &fastHTTPResponseWriter{ctx: ctx}
|
||||
|
||||
h := rw.Header()
|
||||
if h == nil {
|
||||
t.Error("Expected non-nil header")
|
||||
}
|
||||
|
||||
h.Set("Content-Type", "text/html")
|
||||
if rw.Header().Get("Content-Type") != "text/html" {
|
||||
t.Errorf("Expected Content-Type text/html, got %s", rw.Header().Get("Content-Type"))
|
||||
}
|
||||
}
|
||||
|
||||
// TestWrap_RoundTrip 完整流程测试
|
||||
func TestWrap_RoundTrip(t *testing.T) {
|
||||
adapter := NewAdapter()
|
||||
|
||||
// fasthttp handler
|
||||
fastHandler := func(ctx *fasthttp.RequestCtx) {
|
||||
// 检查请求
|
||||
if string(ctx.Method()) != "POST" {
|
||||
t.Errorf("Expected POST method, got %s", ctx.Method())
|
||||
}
|
||||
|
||||
// 设置响应
|
||||
ctx.SetStatusCode(201)
|
||||
ctx.SetBodyString("Created")
|
||||
ctx.Response.Header.Set("X-Response-Header", "test-value")
|
||||
}
|
||||
|
||||
httpHandler := adapter.Wrap(fastHandler)
|
||||
|
||||
// 创建请求
|
||||
req := &http.Request{
|
||||
Method: "POST",
|
||||
URL: &url.URL{Path: "/create"},
|
||||
Host: "localhost",
|
||||
Header: http.Header{
|
||||
"Content-Type": []string{"application/json"},
|
||||
},
|
||||
Body: io.NopCloser(bytes.NewReader([]byte("request data"))),
|
||||
}
|
||||
|
||||
// 创建 mock ResponseWriter
|
||||
rw := &mockResponseWriter{}
|
||||
|
||||
// 执行
|
||||
httpHandler.ServeHTTP(rw, req)
|
||||
|
||||
// 检查响应
|
||||
if rw.status != 201 {
|
||||
t.Errorf("Expected status 201, got %d", rw.status)
|
||||
}
|
||||
|
||||
if rw.header.Get("X-Response-Header") != "test-value" {
|
||||
t.Errorf("Expected X-Response-Header test-value, got %s", rw.header.Get("X-Response-Header"))
|
||||
}
|
||||
|
||||
if string(rw.body) != "Created" {
|
||||
t.Errorf("Expected body 'Created', got %s", rw.body)
|
||||
}
|
||||
}
|
||||
|
||||
// mockResponseWriter 用于测试的 mock ResponseWriter
|
||||
type mockResponseWriter struct {
|
||||
status int
|
||||
header http.Header
|
||||
body []byte
|
||||
}
|
||||
|
||||
func (m *mockResponseWriter) Header() http.Header {
|
||||
if m.header == nil {
|
||||
m.header = make(http.Header)
|
||||
}
|
||||
return m.header
|
||||
}
|
||||
|
||||
func (m *mockResponseWriter) WriteHeader(statusCode int) {
|
||||
m.status = statusCode
|
||||
}
|
||||
|
||||
func (m *mockResponseWriter) Write(data []byte) (int, error) {
|
||||
m.body = append(m.body, data...)
|
||||
if m.status == 0 {
|
||||
m.status = 200
|
||||
}
|
||||
return len(data), nil
|
||||
}
|
||||
@ -268,10 +268,10 @@ func (s *Server) GetAltSvcHeader() string {
|
||||
|
||||
// Stats 返回服务器统计信息。
|
||||
type Stats struct {
|
||||
Running bool // 是否运行中
|
||||
Listen string // 监听地址
|
||||
Enable0RTT bool // 是否启用 0-RTT
|
||||
MaxStreams int // 最大并发流
|
||||
Running bool // 是否运行中
|
||||
Listen string // 监听地址
|
||||
Enable0RTT bool // 是否启用 0-RTT
|
||||
MaxStreams int // 最大并发流
|
||||
}
|
||||
|
||||
// GetStats 返回服务器统计信息。
|
||||
@ -285,4 +285,4 @@ func (s *Server) GetStats() Stats {
|
||||
Enable0RTT: s.config.Enable0RTT,
|
||||
MaxStreams: s.config.MaxStreams,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
374
internal/http3/server_test.go
Normal file
374
internal/http3/server_test.go
Normal file
@ -0,0 +1,374 @@
|
||||
package http3
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/valyala/fasthttp"
|
||||
"rua.plus/lolly/internal/config"
|
||||
)
|
||||
|
||||
// TestNewServer_NilConfig 测试空配置错误
|
||||
func TestNewServer_NilConfig(t *testing.T) {
|
||||
handler := func(ctx *fasthttp.RequestCtx) {}
|
||||
|
||||
server, err := NewServer(nil, handler, &tls.Config{})
|
||||
|
||||
if err == nil {
|
||||
t.Error("Expected error for nil config")
|
||||
}
|
||||
if server != nil {
|
||||
t.Error("Expected nil server for nil config")
|
||||
}
|
||||
if err.Error() != "http3 config is nil" {
|
||||
t.Errorf("Expected error message 'http3 config is nil', got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestNewServer_NilHandler 测试空 handler 错误
|
||||
func TestNewServer_NilHandler(t *testing.T) {
|
||||
cfg := &config.HTTP3Config{
|
||||
Enabled: true,
|
||||
Listen: ":443",
|
||||
Enable0RTT: true,
|
||||
}
|
||||
|
||||
server, err := NewServer(cfg, nil, &tls.Config{})
|
||||
|
||||
if err == nil {
|
||||
t.Error("Expected error for nil handler")
|
||||
}
|
||||
if server != nil {
|
||||
t.Error("Expected nil server for nil handler")
|
||||
}
|
||||
if err.Error() != "handler is nil" {
|
||||
t.Errorf("Expected error message 'handler is nil', got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestNewServer_NilTLS 测试空 TLS 配置错误
|
||||
func TestNewServer_NilTLS(t *testing.T) {
|
||||
cfg := &config.HTTP3Config{
|
||||
Enabled: true,
|
||||
Listen: ":443",
|
||||
Enable0RTT: true,
|
||||
}
|
||||
handler := func(ctx *fasthttp.RequestCtx) {}
|
||||
|
||||
server, err := NewServer(cfg, handler, nil)
|
||||
|
||||
if err == nil {
|
||||
t.Error("Expected error for nil TLS config")
|
||||
}
|
||||
if server != nil {
|
||||
t.Error("Expected nil server for nil TLS config")
|
||||
}
|
||||
if err.Error() != "tls config is required for HTTP/3" {
|
||||
t.Errorf("Expected error message 'tls config is required for HTTP/3', got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestNewServer_Success 测试成功创建服务器
|
||||
func TestNewServer_Success(t *testing.T) {
|
||||
cfg := &config.HTTP3Config{
|
||||
Enabled: true,
|
||||
Listen: ":443",
|
||||
Enable0RTT: true,
|
||||
MaxStreams: 100,
|
||||
}
|
||||
handler := func(ctx *fasthttp.RequestCtx) {}
|
||||
tlsConfig := &tls.Config{
|
||||
Certificates: []tls.Certificate{},
|
||||
}
|
||||
|
||||
server, err := NewServer(cfg, handler, tlsConfig)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
if server == nil {
|
||||
t.Error("Expected non-nil server")
|
||||
}
|
||||
|
||||
if server.config != cfg {
|
||||
t.Error("Config not set correctly")
|
||||
}
|
||||
|
||||
if server.handler == nil {
|
||||
t.Error("Handler not set correctly")
|
||||
}
|
||||
|
||||
if server.adapter == nil {
|
||||
t.Error("Adapter not initialized")
|
||||
}
|
||||
|
||||
if server.tlsConfig != tlsConfig {
|
||||
t.Error("TLS config not set correctly")
|
||||
}
|
||||
|
||||
if server.running {
|
||||
t.Error("Server should not be running initially")
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetAltSvcHeader_DefaultPort 测试默认端口 Alt-Svc 头
|
||||
func TestGetAltSvcHeader_DefaultPort(t *testing.T) {
|
||||
cfg := &config.HTTP3Config{
|
||||
Enabled: true,
|
||||
Listen: ":443",
|
||||
}
|
||||
handler := func(ctx *fasthttp.RequestCtx) {}
|
||||
|
||||
server, _ := NewServer(cfg, handler, &tls.Config{})
|
||||
|
||||
header := server.GetAltSvcHeader()
|
||||
|
||||
expected := `h3=":443"; ma=86400`
|
||||
if header != expected {
|
||||
t.Errorf("Expected Alt-Svc header '%s', got '%s'", expected, header)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetAltSvcHeader_CustomPort 测试自定义端口 Alt-Svc 头
|
||||
func TestGetAltSvcHeader_CustomPort(t *testing.T) {
|
||||
cfg := &config.HTTP3Config{
|
||||
Enabled: true,
|
||||
Listen: ":8443",
|
||||
}
|
||||
handler := func(ctx *fasthttp.RequestCtx) {}
|
||||
|
||||
server, _ := NewServer(cfg, handler, &tls.Config{})
|
||||
|
||||
header := server.GetAltSvcHeader()
|
||||
|
||||
expected := `h3=":8443"; ma=86400`
|
||||
if header != expected {
|
||||
t.Errorf("Expected Alt-Svc header '%s', got '%s'", expected, header)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetAltSvcHeader_Disabled 测试禁用 HTTP/3 时返回空
|
||||
func TestGetAltSvcHeader_Disabled(t *testing.T) {
|
||||
cfg := &config.HTTP3Config{
|
||||
Enabled: false,
|
||||
Listen: ":443",
|
||||
}
|
||||
handler := func(ctx *fasthttp.RequestCtx) {}
|
||||
|
||||
server, _ := NewServer(cfg, handler, &tls.Config{})
|
||||
|
||||
header := server.GetAltSvcHeader()
|
||||
|
||||
if header != "" {
|
||||
t.Errorf("Expected empty Alt-Svc header when disabled, got '%s'", header)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetAltSvcHeader_EmptyListen 测试空监听地址时使用默认端口
|
||||
func TestGetAltSvcHeader_EmptyListen(t *testing.T) {
|
||||
cfg := &config.HTTP3Config{
|
||||
Enabled: true,
|
||||
Listen: "", // 空,使用默认 :443
|
||||
}
|
||||
handler := func(ctx *fasthttp.RequestCtx) {}
|
||||
|
||||
server, _ := NewServer(cfg, handler, &tls.Config{})
|
||||
|
||||
header := server.GetAltSvcHeader()
|
||||
|
||||
expected := `h3=":443"; ma=86400`
|
||||
if header != expected {
|
||||
t.Errorf("Expected Alt-Svc header '%s', got '%s'", expected, header)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetStats 测试获取统计信息
|
||||
func TestGetStats(t *testing.T) {
|
||||
cfg := &config.HTTP3Config{
|
||||
Enabled: true,
|
||||
Listen: ":8443",
|
||||
Enable0RTT: true,
|
||||
MaxStreams: 200,
|
||||
}
|
||||
handler := func(ctx *fasthttp.RequestCtx) {}
|
||||
|
||||
server, _ := NewServer(cfg, handler, &tls.Config{})
|
||||
|
||||
stats := server.GetStats()
|
||||
|
||||
if stats.Running {
|
||||
t.Error("Expected Running to be false initially")
|
||||
}
|
||||
|
||||
if stats.Listen != ":8443" {
|
||||
t.Errorf("Expected Listen ':8443', got '%s'", stats.Listen)
|
||||
}
|
||||
|
||||
if !stats.Enable0RTT {
|
||||
t.Error("Expected Enable0RTT to be true")
|
||||
}
|
||||
|
||||
if stats.MaxStreams != 200 {
|
||||
t.Errorf("Expected MaxStreams 200, got %d", stats.MaxStreams)
|
||||
}
|
||||
}
|
||||
|
||||
// TestIsRunning 测试运行状态检查
|
||||
func TestIsRunning(t *testing.T) {
|
||||
cfg := &config.HTTP3Config{
|
||||
Enabled: true,
|
||||
Listen: ":443",
|
||||
}
|
||||
handler := func(ctx *fasthttp.RequestCtx) {}
|
||||
|
||||
server, _ := NewServer(cfg, handler, &tls.Config{})
|
||||
|
||||
// 初始状态应该是 false
|
||||
if server.IsRunning() {
|
||||
t.Error("Expected IsRunning to be false initially")
|
||||
}
|
||||
|
||||
// 手动设置运行状态(不启动真实服务器)
|
||||
server.mu.Lock()
|
||||
server.running = true
|
||||
server.mu.Unlock()
|
||||
|
||||
if !server.IsRunning() {
|
||||
t.Error("Expected IsRunning to be true after setting")
|
||||
}
|
||||
|
||||
// 再次设置为 false
|
||||
server.mu.Lock()
|
||||
server.running = false
|
||||
server.mu.Unlock()
|
||||
|
||||
if server.IsRunning() {
|
||||
t.Error("Expected IsRunning to be false after unsetting")
|
||||
}
|
||||
}
|
||||
|
||||
// TestStop_NotRunning 测试停止未运行的服务器
|
||||
func TestStop_NotRunning(t *testing.T) {
|
||||
cfg := &config.HTTP3Config{
|
||||
Enabled: true,
|
||||
Listen: ":443",
|
||||
}
|
||||
handler := func(ctx *fasthttp.RequestCtx) {}
|
||||
|
||||
server, _ := NewServer(cfg, handler, &tls.Config{})
|
||||
|
||||
// 服务器未启动,Stop 应该返回 nil
|
||||
err := server.Stop()
|
||||
if err != nil {
|
||||
t.Errorf("Expected nil error when stopping non-running server, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGracefulStop_NotRunning 测试优雅停止未运行的服务器
|
||||
func TestGracefulStop_NotRunning(t *testing.T) {
|
||||
cfg := &config.HTTP3Config{
|
||||
Enabled: true,
|
||||
Listen: ":443",
|
||||
}
|
||||
handler := func(ctx *fasthttp.RequestCtx) {}
|
||||
|
||||
server, _ := NewServer(cfg, handler, &tls.Config{})
|
||||
|
||||
// 服务器未启动,GracefulStop 应该返回 nil
|
||||
err := server.GracefulStop(5 * time.Second)
|
||||
if err != nil {
|
||||
t.Errorf("Expected nil error when graceful stopping non-running server, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGracefulStop_WithTimeout 测试优雅停止超时
|
||||
func TestGracefulStop_WithTimeout(t *testing.T) {
|
||||
cfg := &config.HTTP3Config{
|
||||
Enabled: true,
|
||||
Listen: ":443",
|
||||
}
|
||||
handler := func(ctx *fasthttp.RequestCtx) {}
|
||||
|
||||
server, _ := NewServer(cfg, handler, &tls.Config{})
|
||||
|
||||
// 测试不同超时值
|
||||
tests := []struct {
|
||||
name string
|
||||
timeout time.Duration
|
||||
}{
|
||||
{"zero timeout", 0},
|
||||
{"short timeout", 100 * time.Millisecond},
|
||||
{"long timeout", 30 * time.Second},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := server.GracefulStop(tt.timeout)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestServer_MultipleStop 测试多次调用 Stop
|
||||
func TestServer_MultipleStop(t *testing.T) {
|
||||
cfg := &config.HTTP3Config{
|
||||
Enabled: true,
|
||||
Listen: ":443",
|
||||
}
|
||||
handler := func(ctx *fasthttp.RequestCtx) {}
|
||||
|
||||
server, _ := NewServer(cfg, handler, &tls.Config{})
|
||||
|
||||
// 多次调用 Stop 应该都是安全的
|
||||
for i := 0; i < 3; i++ {
|
||||
err := server.Stop()
|
||||
if err != nil {
|
||||
t.Errorf("Stop call %d returned error: %v", i+1, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestServer_MultipleGracefulStop 测试多次调用 GracefulStop
|
||||
func TestServer_MultipleGracefulStop(t *testing.T) {
|
||||
cfg := &config.HTTP3Config{
|
||||
Enabled: true,
|
||||
Listen: ":443",
|
||||
}
|
||||
handler := func(ctx *fasthttp.RequestCtx) {}
|
||||
|
||||
server, _ := NewServer(cfg, handler, &tls.Config{})
|
||||
|
||||
// 多次调用 GracefulStop 应该都是安全的
|
||||
for i := 0; i < 3; i++ {
|
||||
err := server.GracefulStop(1 * time.Second)
|
||||
if err != nil {
|
||||
t.Errorf("GracefulStop call %d returned error: %v", i+1, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestStats_Struct 测试 Stats 结构体
|
||||
func TestStats_Struct(t *testing.T) {
|
||||
stats := Stats{
|
||||
Running: true,
|
||||
Listen: ":443",
|
||||
Enable0RTT: true,
|
||||
MaxStreams: 100,
|
||||
}
|
||||
|
||||
if !stats.Running {
|
||||
t.Error("Expected Running true")
|
||||
}
|
||||
if stats.Listen != ":443" {
|
||||
t.Errorf("Expected Listen ':443', got '%s'", stats.Listen)
|
||||
}
|
||||
if !stats.Enable0RTT {
|
||||
t.Error("Expected Enable0RTT true")
|
||||
}
|
||||
if stats.MaxStreams != 100 {
|
||||
t.Errorf("Expected MaxStreams 100, got %d", stats.MaxStreams)
|
||||
}
|
||||
}
|
||||
@ -163,9 +163,9 @@ func (c *ConsistentHash) GetVirtualNodes() int {
|
||||
|
||||
// Stats 返回一致性哈希统计信息。
|
||||
type ConsistentHashStats struct {
|
||||
VirtualNodes int // 虚拟节点数
|
||||
CircleSize int // 哈希环大小
|
||||
SortedHashes int // 排序哈希值数量
|
||||
VirtualNodes int // 虚拟节点数
|
||||
CircleSize int // 哈希环大小
|
||||
SortedHashes int // 排序哈希值数量
|
||||
}
|
||||
|
||||
// GetStats 返回统计信息。
|
||||
@ -174,11 +174,11 @@ func (c *ConsistentHash) GetStats() ConsistentHashStats {
|
||||
defer c.mu.RUnlock()
|
||||
|
||||
return ConsistentHashStats{
|
||||
VirtualNodes: c.virtualNodes,
|
||||
CircleSize: len(c.circle),
|
||||
SortedHashes: len(c.sortedHashes),
|
||||
VirtualNodes: c.virtualNodes,
|
||||
CircleSize: len(c.circle),
|
||||
SortedHashes: len(c.sortedHashes),
|
||||
}
|
||||
}
|
||||
|
||||
// 验证接口实现
|
||||
var _ Balancer = (*ConsistentHash)(nil)
|
||||
var _ Balancer = (*ConsistentHash)(nil)
|
||||
|
||||
@ -25,7 +25,7 @@ type Logger struct {
|
||||
|
||||
// AppLogger 应用日志管理器,统一管理启动/停止日志。
|
||||
type AppLogger struct {
|
||||
format string // "text" 或 "json"
|
||||
format string // "text" 或 "json"
|
||||
errorLog zerolog.Logger
|
||||
writer io.Writer
|
||||
}
|
||||
@ -121,15 +121,15 @@ func (l *Logger) formatAccessLog(ctx *fasthttp.RequestCtx, status int, size int6
|
||||
}
|
||||
|
||||
replacements := map[string]string{
|
||||
"$remote_addr": ctx.RemoteAddr().String(),
|
||||
"$remote_user": remoteUser,
|
||||
"$request": string(ctx.Method()) + " " + string(ctx.Path()) + " " + string(ctx.Request.Header.Protocol()),
|
||||
"$status": strconv.Itoa(status),
|
||||
"$body_bytes_sent": strconv.FormatInt(size, 10),
|
||||
"$request_time": fmt.Sprintf("%.6f", duration.Seconds()),
|
||||
"$http_referer": string(ctx.Request.Header.Peek("Referer")),
|
||||
"$http_user_agent": string(ctx.Request.Header.Peek("User-Agent")),
|
||||
"$time": time.Now().Format(time.RFC3339),
|
||||
"$remote_addr": ctx.RemoteAddr().String(),
|
||||
"$remote_user": remoteUser,
|
||||
"$request": string(ctx.Method()) + " " + string(ctx.Path()) + " " + string(ctx.Request.Header.Protocol()),
|
||||
"$status": strconv.Itoa(status),
|
||||
"$body_bytes_sent": strconv.FormatInt(size, 10),
|
||||
"$request_time": fmt.Sprintf("%.6f", duration.Seconds()),
|
||||
"$http_referer": string(ctx.Request.Header.Peek("Referer")),
|
||||
"$http_user_agent": string(ctx.Request.Header.Peek("User-Agent")),
|
||||
"$time": time.Now().Format(time.RFC3339),
|
||||
}
|
||||
|
||||
result := l.accessFormat
|
||||
|
||||
@ -144,4 +144,4 @@ func (g *GzipStatic) Extensions() []string {
|
||||
// DefaultExtensions 返回默认支持的扩展名。
|
||||
func DefaultExtensions() []string {
|
||||
return []string{".html", ".css", ".js", ".json", ".xml", ".svg", ".txt"}
|
||||
}
|
||||
}
|
||||
|
||||
@ -40,9 +40,9 @@ type SlidingWindowLimiter struct {
|
||||
|
||||
// windowCounter 窗口计数器。
|
||||
type windowCounter struct {
|
||||
count int64
|
||||
count int64
|
||||
timestamps []time.Time // precise 模式下记录每个请求时间
|
||||
mu sync.Mutex
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// NewSlidingWindowLimiter 创建滑动窗口限流器。
|
||||
@ -260,4 +260,4 @@ func (s *SlidingWindowLimiter) GetCount(key string) int {
|
||||
}
|
||||
|
||||
return int(counter.count)
|
||||
}
|
||||
}
|
||||
|
||||
@ -297,4 +297,4 @@ func TestCopyData(t *testing.T) {
|
||||
case <-time.After(1 * time.Second):
|
||||
t.Error("copyData did not complete in time")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/valyala/fasthttp"
|
||||
"rua.plus/lolly/internal/config"
|
||||
)
|
||||
|
||||
@ -278,4 +280,211 @@ func TestBuildMiddlewareChain_AllMiddlewares(t *testing.T) {
|
||||
if chain == nil {
|
||||
t.Error("Expected non-nil chain")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestTrackStats 测试请求统计追踪
|
||||
func TestTrackStats(t *testing.T) {
|
||||
cfg := &config.Config{
|
||||
Server: config.ServerConfig{
|
||||
Listen: ":8080",
|
||||
},
|
||||
}
|
||||
|
||||
s := New(cfg)
|
||||
|
||||
// 初始统计应该为 0
|
||||
if s.requests.Load() != 0 {
|
||||
t.Error("Initial requests should be 0")
|
||||
}
|
||||
if s.bytesSent.Load() != 0 {
|
||||
t.Error("Initial bytesSent should be 0")
|
||||
}
|
||||
if s.bytesReceived.Load() != 0 {
|
||||
t.Error("Initial bytesReceived should be 0")
|
||||
}
|
||||
|
||||
// 创建测试 handler
|
||||
handler := func(ctx *fasthttp.RequestCtx) {
|
||||
ctx.SetBodyString("response body")
|
||||
}
|
||||
|
||||
// 包装 handler
|
||||
wrappedHandler := s.trackStats(handler)
|
||||
|
||||
// 创建测试请求上下文
|
||||
ctx := &fasthttp.RequestCtx{}
|
||||
ctx.Init(&fasthttp.Request{}, nil, nil)
|
||||
ctx.Request.SetBody([]byte("request body"))
|
||||
|
||||
// 执行
|
||||
wrappedHandler(ctx)
|
||||
|
||||
// 验证统计
|
||||
if s.requests.Load() != 1 {
|
||||
t.Errorf("Expected 1 request, got %d", s.requests.Load())
|
||||
}
|
||||
|
||||
if s.bytesReceived.Load() != int64(len("request body")) {
|
||||
t.Errorf("Expected bytesReceived %d, got %d", len("request body"), s.bytesReceived.Load())
|
||||
}
|
||||
|
||||
if s.bytesSent.Load() != int64(len("response body")) {
|
||||
t.Errorf("Expected bytesSent %d, got %d", len("response body"), s.bytesSent.Load())
|
||||
}
|
||||
}
|
||||
|
||||
// TestTrackStats_MultipleRequests 测试多次请求统计
|
||||
func TestTrackStats_MultipleRequests(t *testing.T) {
|
||||
cfg := &config.Config{
|
||||
Server: config.ServerConfig{
|
||||
Listen: ":8080",
|
||||
},
|
||||
}
|
||||
|
||||
s := New(cfg)
|
||||
|
||||
handler := func(ctx *fasthttp.RequestCtx) {
|
||||
ctx.SetBodyString("ok")
|
||||
}
|
||||
|
||||
wrappedHandler := s.trackStats(handler)
|
||||
|
||||
// 执行多次请求
|
||||
for i := 0; i < 10; i++ {
|
||||
ctx := &fasthttp.RequestCtx{}
|
||||
ctx.Init(&fasthttp.Request{}, nil, nil)
|
||||
wrappedHandler(ctx)
|
||||
}
|
||||
|
||||
if s.requests.Load() != 10 {
|
||||
t.Errorf("Expected 10 requests, got %d", s.requests.Load())
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetListeners_Empty 测试空监听器列表
|
||||
func TestGetListeners_Empty(t *testing.T) {
|
||||
cfg := &config.Config{
|
||||
Server: config.ServerConfig{
|
||||
Listen: ":8080",
|
||||
},
|
||||
}
|
||||
|
||||
s := New(cfg)
|
||||
|
||||
listeners := s.GetListeners()
|
||||
if listeners != nil {
|
||||
t.Errorf("Expected nil listeners, got %v", listeners)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSetListeners 测试设置监听器
|
||||
func TestSetListeners(t *testing.T) {
|
||||
cfg := &config.Config{
|
||||
Server: config.ServerConfig{
|
||||
Listen: ":8080",
|
||||
},
|
||||
}
|
||||
|
||||
s := New(cfg)
|
||||
|
||||
// 创建模拟监听器
|
||||
listener1, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create listener: %v", err)
|
||||
}
|
||||
defer listener1.Close()
|
||||
|
||||
listener2, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create listener: %v", err)
|
||||
}
|
||||
defer listener2.Close()
|
||||
|
||||
listeners := []net.Listener{listener1, listener2}
|
||||
s.SetListeners(listeners)
|
||||
|
||||
// 验证设置成功
|
||||
got := s.GetListeners()
|
||||
if len(got) != 2 {
|
||||
t.Errorf("Expected 2 listeners, got %d", len(got))
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetTLSConfig_NotConfigured 测试未配置 TLS
|
||||
func TestGetTLSConfig_NotConfigured(t *testing.T) {
|
||||
cfg := &config.Config{
|
||||
Server: config.ServerConfig{
|
||||
Listen: ":8080",
|
||||
},
|
||||
}
|
||||
|
||||
s := New(cfg)
|
||||
|
||||
tlsConfig, err := s.GetTLSConfig()
|
||||
if err == nil {
|
||||
t.Error("Expected error for unconfigured TLS")
|
||||
}
|
||||
if tlsConfig != nil {
|
||||
t.Error("Expected nil TLS config")
|
||||
}
|
||||
if err.Error() != "TLS not configured" {
|
||||
t.Errorf("Expected error 'TLS not configured', got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetHandler 测试获取 handler
|
||||
func TestGetHandler(t *testing.T) {
|
||||
cfg := &config.Config{
|
||||
Server: config.ServerConfig{
|
||||
Listen: ":8080",
|
||||
},
|
||||
}
|
||||
|
||||
s := New(cfg)
|
||||
|
||||
// 初始 handler 应该为 nil
|
||||
handler := s.GetHandler()
|
||||
if handler != nil {
|
||||
t.Error("Expected nil handler initially")
|
||||
}
|
||||
|
||||
// 设置一个 handler
|
||||
testHandler := func(ctx *fasthttp.RequestCtx) {
|
||||
ctx.SetBodyString("test")
|
||||
}
|
||||
s.handler = testHandler
|
||||
|
||||
// 验证获取成功
|
||||
got := s.GetHandler()
|
||||
if got == nil {
|
||||
t.Error("Expected non-nil handler after setting")
|
||||
}
|
||||
}
|
||||
|
||||
// TestServer_Connections 测试连接统计
|
||||
func TestServer_Connections(t *testing.T) {
|
||||
cfg := &config.Config{
|
||||
Server: config.ServerConfig{
|
||||
Listen: ":8080",
|
||||
},
|
||||
}
|
||||
|
||||
s := New(cfg)
|
||||
|
||||
// 初始连接数应该为 0
|
||||
if s.connections.Load() != 0 {
|
||||
t.Error("Initial connections should be 0")
|
||||
}
|
||||
|
||||
// 增加
|
||||
s.connections.Add(1)
|
||||
if s.connections.Load() != 1 {
|
||||
t.Errorf("Expected 1 connection, got %d", s.connections.Load())
|
||||
}
|
||||
|
||||
// 减少
|
||||
s.connections.Add(-1)
|
||||
if s.connections.Load() != 0 {
|
||||
t.Errorf("Expected 0 connections, got %d", s.connections.Load())
|
||||
}
|
||||
}
|
||||
|
||||
@ -478,4 +478,4 @@ func TestCollectStatus_WithFileCache(t *testing.T) {
|
||||
if status.Cache != nil {
|
||||
t.Error("expected Cache to be nil when no fileCache configured")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user