lolly/internal/server/upgrade_test.go
xfy 9f7090df67 test(handler,middleware,server,ssl,proxy): 扩展测试覆盖率
- handler: 添加 sendfile 和 static 处理器测试
- middleware/security: 添加访问控制、认证、请求头、限流测试
- server: 添加池、pprof、清理、状态、升级、vhost 测试
- ssl: 添加客户端验证、OCSP、SSL 测试
- proxy: 添加代理覆盖率补充测试

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-22 10:42:05 +08:00

599 lines
16 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 !windows
// Package server 提供优雅升级功能的测试。
//
// 该文件测试优雅升级模块的各项功能,包括:
// - UpgradeManager 的创建和配置
// - 子进程检测
// - PID 文件的读写
// - 监听器继承
// - 进程通知机制
// - 平滑关闭等待
// - 错误处理场景
//
// 作者xfy
package server
import (
"net"
"os"
"syscall"
"testing"
"time"
)
func TestNewUpgradeManager(t *testing.T) {
srv := New(nil)
mgr := NewUpgradeManager(srv)
if mgr == nil {
t.Fatal("Expected non-nil manager")
}
if mgr.server != srv {
t.Error("Expected server to be set")
}
}
func TestIsChild(t *testing.T) {
mgr := NewUpgradeManager(nil)
// 默认不是子进程
if mgr.IsChild() {
t.Error("Expected IsChild to be false by default")
}
// 设置环境变量
t.Setenv("GRACEFUL_UPGRADE", "1")
if !mgr.IsChild() {
t.Error("Expected IsChild to be true when GRACEFUL_UPGRADE=1")
}
}
func TestPidFile(t *testing.T) {
tmpFile := "/tmp/lolly-test.pid"
defer func() {
_ = os.Remove(tmpFile)
}()
mgr := NewUpgradeManager(nil)
mgr.SetPidFile(tmpFile)
// 写入 PID
if err := mgr.WritePid(); err != nil {
t.Errorf("WritePid failed: %v", err)
}
// 读取 PID
pid, err := mgr.ReadOldPid()
if err != nil {
t.Errorf("ReadOldPid failed: %v", err)
}
if pid != os.Getpid() {
t.Errorf("Expected pid %d, got %d", os.Getpid(), pid)
}
}
func TestReadOldPidNoFile(t *testing.T) {
mgr := NewUpgradeManager(nil)
mgr.SetPidFile("/nonexistent/path/pid")
_, err := mgr.ReadOldPid()
if err == nil {
t.Error("Expected error for non-existent file")
}
}
func TestGetInheritedListenersNoFds(t *testing.T) {
mgr := NewUpgradeManager(nil)
// 没有设置 LISTEN_FDS
listeners, err := mgr.GetInheritedListeners()
if err != nil {
t.Errorf("GetInheritedListeners failed: %v", err)
}
if len(listeners) != 0 {
t.Errorf("Expected 0 listeners, got %d", len(listeners))
}
}
func TestNotifyOldProcessNoPid(t *testing.T) {
mgr := NewUpgradeManager(nil)
mgr.SetPidFile("/nonexistent/pid")
// 没有 PID 文件,应该返回 nil
err := mgr.NotifyOldProcess()
if err != nil {
t.Errorf("NotifyOldProcess should return nil for no pid, got: %v", err)
}
}
func TestWaitForShutdownNoOldPid(t *testing.T) {
mgr := NewUpgradeManager(nil)
// 没有旧进程
err := mgr.WaitForShutdown(0)
if err != nil {
t.Errorf("WaitForShutdown should return nil for no old pid, got: %v", err)
}
}
// TestUpgradeSetListeners 测试监听器设置
func TestUpgradeSetListeners(t *testing.T) {
mgr := NewUpgradeManager(nil)
// 创建模拟监听器
listener1, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("Failed to create listener: %v", err)
}
defer func() {
_ = listener1.Close()
}()
listener2, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("Failed to create listener: %v", err)
}
defer func() {
_ = listener2.Close()
}()
listeners := []net.Listener{listener1, listener2}
mgr.SetListeners(listeners)
if len(mgr.listeners) != 2 {
t.Errorf("Expected 2 listeners, got %d", len(mgr.listeners))
}
}
// TestWritePid_NoPidFile 测试无 PID 文件配置时的行为
func TestWritePid_NoPidFile(t *testing.T) {
mgr := NewUpgradeManager(nil)
// 不设置 PID 文件
err := mgr.WritePid()
if err != nil {
t.Errorf("WritePid should return nil when no pid file configured, got: %v", err)
}
}
// TestReadOldPid_InvalidContent 测试 PID 文件内容无效时的错误处理
func TestReadOldPid_InvalidContent(t *testing.T) {
tmpFile := "/tmp/lolly-test-invalid.pid"
defer func() {
_ = os.Remove(tmpFile)
}()
// 写入无效内容
if err := os.WriteFile(tmpFile, []byte("not-a-pid"), 0o644); err != nil {
t.Fatalf("Failed to write temp file: %v", err)
}
mgr := NewUpgradeManager(nil)
mgr.SetPidFile(tmpFile)
_, err := mgr.ReadOldPid()
if err == nil {
t.Error("Expected error for invalid PID content")
}
}
// TestGetInheritedListeners_InvalidFds 测试 LISTEN_FDS 环境变量格式无效
func TestGetInheritedListeners_InvalidFds(t *testing.T) {
tests := []struct {
name string
fdsEnv string
wantErr bool
}{
{
name: "invalid format - not a number",
fdsEnv: "invalid",
wantErr: true,
},
{
name: "zero fds",
fdsEnv: "0",
wantErr: false,
},
{
name: "negative fds",
fdsEnv: "-1",
wantErr: false, // Sscanf 解析成功,但逻辑上无效
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Setenv("LISTEN_FDS", tt.fdsEnv)
mgr := NewUpgradeManager(nil)
_, err := mgr.GetInheritedListeners()
if tt.wantErr {
if err == nil {
t.Error("Expected error for invalid LISTEN_FDS")
}
}
// 零或负数应该返回空列表,无错误
// 注意:对于 -1后续逻辑可能会尝试访问无效的 FD
})
}
}
// TestWaitForShutdown_WithTimeout 测试超时行为
func TestWaitForShutdown_WithTimeout(t *testing.T) {
mgr := NewUpgradeManager(nil)
mgr.oldPid = 9999999 // 不存在的进程 ID
// 短超时 - 不存在的进程会被立即检测到
// WaitForShutdown 会尝试发送 Signal(0) 检测进程存在
err := mgr.WaitForShutdown(100 * time.Millisecond)
// 对于不存在的进程Signal(0) 会返回错误,所以 WaitForShutdown 应该返回 nil
if err != nil {
// 进程不存在时Signal(0) 返回错误,函数提前返回 nil
t.Logf("WaitForShutdown returned: %v (expected nil for non-existent process)", err)
}
}
// TestListenerFile_TCPListener 测试从 TCP 监听器获取文件
func TestListenerFile_TCPListener(t *testing.T) {
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("Failed to create listener: %v", err)
}
defer func() {
_ = listener.Close()
}()
file, err := listenerFile(listener)
if err != nil {
t.Errorf("Failed to get listener file: %v", err)
}
if file != nil {
_ = file.Close()
}
}
// TestListenerFile_UnsupportedType 测试不支持的监听器类型
func TestListenerFile_UnsupportedType(t *testing.T) {
// 创建一个模拟的不支持类型
listener := &mockListener{}
_, err := listenerFile(listener)
if err == nil {
t.Error("Expected error for unsupported listener type")
}
}
// mockListener 是一个不实现 TCPListener/UnixListener 的监听器
type mockListener struct{}
func (m *mockListener) Accept() (net.Conn, error) { return nil, nil }
func (m *mockListener) Close() error { return nil }
func (m *mockListener) Addr() net.Addr { return nil }
// TestGracefulUpgrade_NoListeners 测试无监听器时的升级失败
func TestGracefulUpgrade_NoListeners(t *testing.T) {
mgr := NewUpgradeManager(nil)
// 不设置监听器
err := mgr.GracefulUpgrade("/nonexistent/binary")
if err == nil {
t.Error("Expected error when no listeners configured")
}
expectedErr := "no listeners configured for upgrade"
if err != nil && err.Error() != expectedErr {
t.Errorf("Expected error '%s', got: %v", expectedErr, err)
}
}
// TestNotifyOldProcess_WithCurrentPid 测试通知进程
// 注意:不能向当前进程发送 SIGQUIT会导致测试崩溃
func TestNotifyOldProcess_WithCurrentPid(t *testing.T) {
// 跳过此测试,因为发送 SIGQUIT 给当前进程会导致崩溃
t.Skip("Skipping test that would send SIGQUIT to current process")
}
// TestReadOldPid_EmptyFile 测试空 PID 文件
func TestReadOldPid_EmptyFile(t *testing.T) {
tmpFile := "/tmp/lolly-test-empty.pid"
defer func() {
_ = os.Remove(tmpFile)
}()
// 写入空内容
if err := os.WriteFile(tmpFile, []byte(""), 0o644); err != nil {
t.Fatalf("Failed to write temp file: %v", err)
}
mgr := NewUpgradeManager(nil)
mgr.SetPidFile(tmpFile)
_, err := mgr.ReadOldPid()
if err == nil {
t.Error("Expected error for empty PID file")
}
}
// TestNotifyOldProcess_ReadPidError 测试读取 PID 失败的情况
func TestNotifyOldProcess_ReadPidError(t *testing.T) {
mgr := NewUpgradeManager(nil)
// 不设置 PID 文件ReadOldPid 会返回错误
err := mgr.NotifyOldProcess()
if err != nil {
t.Errorf("NotifyOldProcess should return nil when ReadOldPid fails, got: %v", err)
}
}
// TestNotifyOldProcess_ZeroPid 测试 PID 为 0 的情况
func TestNotifyOldProcess_ZeroPid(t *testing.T) {
tmpFile := "/tmp/lolly-test-zero.pid"
defer func() {
_ = os.Remove(tmpFile)
}()
// 写入 PID 0
if err := os.WriteFile(tmpFile, []byte("0"), 0o644); err != nil {
t.Fatalf("Failed to write temp file: %v", err)
}
mgr := NewUpgradeManager(nil)
mgr.SetPidFile(tmpFile)
err := mgr.NotifyOldProcess()
if err != nil {
t.Errorf("NotifyOldProcess should return nil for PID 0, got: %v", err)
}
}
// TestNotifyOldProcess_NonExistentProcess 测试通知不存在的进程
func TestNotifyOldProcess_NonExistentProcess(t *testing.T) {
tmpFile := "/tmp/lolly-test-nonexistent.pid"
defer func() {
_ = os.Remove(tmpFile)
}()
// 写入一个不存在的 PID (使用一个极大的 PID 值)
if err := os.WriteFile(tmpFile, []byte("9999999"), 0o644); err != nil {
t.Fatalf("Failed to write temp file: %v", err)
}
mgr := NewUpgradeManager(nil)
mgr.SetPidFile(tmpFile)
// 对于不存在的进程Signal 会返回错误
// NotifyOldProcess 会直接返回这个错误
err := mgr.NotifyOldProcess()
if err == nil {
t.Error("Expected error when notifying non-existent process")
}
}
// TestNotifyOldProcess_FindProcessError 测试 os.FindProcess 的行为
// 注意:在 Unix 系统上os.FindProcess 总是成功,即使进程不存在
func TestNotifyOldProcess_FindProcessBehavior(t *testing.T) {
// 这个测试验证 os.FindProcess 的行为
// 在 Unix 上FindProcess 总是返回一个 Process 对象
process, err := os.FindProcess(9999999)
if err != nil {
t.Errorf("FindProcess should not return error on Unix, got: %v", err)
}
if process == nil {
t.Error("FindProcess should return non-nil Process on Unix")
}
}
// TestSetupSignalHandlers_SetsUpChannel 测试信号处理器设置
func TestSetupSignalHandlers_SetsUpChannel(t *testing.T) {
mgr := NewUpgradeManager(nil)
// 调用 SetupSignalHandlers 应该不会 panic
mgr.SetupSignalHandlers("/nonexistent/binary")
// 给 goroutine 一点时间启动
time.Sleep(10 * time.Millisecond)
// 测试通过如果没 panic
}
// TestSetupSignalHandlers_TriggersUpgrade 测试信号触发升级
func TestSetupSignalHandlers_TriggersUpgrade(t *testing.T) {
mgr := NewUpgradeManager(nil)
// 创建一个监听器
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("Failed to create listener: %v", err)
}
defer func() {
_ = listener.Close()
}()
mgr.SetListeners([]net.Listener{listener})
// 设置信号处理器,使用一个不存在的二进制文件
mgr.SetupSignalHandlers("/nonexistent/binary/path")
// 给 goroutine 启动时间
time.Sleep(10 * time.Millisecond)
// 发送 SIGUSR2 信号给当前进程
// 注意:这会触发 GracefulUpgrade但由于二进制文件不存在会失败
// 信号处理器会忽略错误(使用 _ = u.GracefulUpgrade
process, err := os.FindProcess(os.Getpid())
if err != nil {
t.Fatalf("Failed to find current process: %v", err)
}
// 发送 SIGUSR2
if err := process.Signal(syscall.SIGUSR2); err != nil {
t.Fatalf("Failed to send SIGUSR2: %v", err)
}
// 等待信号处理
time.Sleep(100 * time.Millisecond)
// 测试通过如果没有 panic
}
// TestGracefulUpgrade_UnsupportedListener 测试不支持的监听器类型
func TestGracefulUpgrade_UnsupportedListener(t *testing.T) {
mgr := NewUpgradeManager(nil)
// 使用 mock 监听器(不支持的类型)
mgr.SetListeners([]net.Listener{&mockListener{}})
err := mgr.GracefulUpgrade("/nonexistent/binary")
if err == nil {
t.Error("Expected error for unsupported listener type")
}
if err != nil && !containsString(err.Error(), "unsupported listener type") &&
!containsString(err.Error(), "failed to get listener file") {
t.Errorf("Expected unsupported listener error, got: %v", err)
}
}
// TestGracefulUpgrade_NonexistentBinary 测试不存在的二进制文件
// 注意:此测试使用 mock 监听器避免创建实际网络连接
func TestGracefulUpgrade_NonexistentBinary(t *testing.T) {
mgr := NewUpgradeManager(nil)
// 使用 mock 监听器测试不支持类型的错误路径
mgr.SetListeners([]net.Listener{&mockListener{}})
// 由于 mockListener 是不支持的类型,应该返回错误
err := mgr.GracefulUpgrade("/nonexistent/path/to/binary")
if err == nil {
t.Error("Expected error for unsupported listener type")
}
}
// TestGracefulUpgrade_WithPidFile 测试升级时写入 PID 文件
// 注意:此测试使用 mock 监听器避免创建实际网络连接
func TestGracefulUpgrade_WithPidFile(t *testing.T) {
tmpFile := "/tmp/lolly-test-upgrade.pid"
defer func() {
_ = os.Remove(tmpFile)
}()
mgr := NewUpgradeManager(nil)
mgr.SetPidFile(tmpFile)
// 使用 mock 监听器
mgr.SetListeners([]net.Listener{&mockListener{}})
// 使用不存在的二进制文件,会失败但测试 PID 文件设置逻辑
_ = mgr.GracefulUpgrade("/nonexistent/binary")
// 测试通过如果没有 panic
}
// TestWaitForShutdown_ProcessExits 测试进程退出后的等待
func TestWaitForShutdown_ProcessExits(t *testing.T) {
mgr := NewUpgradeManager(nil)
// 使用一个不存在的 PIDSignal(0) 会返回错误)
mgr.oldPid = 9999999
// 不存在的进程应该立即返回 nil
err := mgr.WaitForShutdown(1 * time.Second)
if err != nil {
t.Errorf("Expected nil for non-existent process, got: %v", err)
}
}
// TestWaitForShutdown_Timeout 测试等待超时
func TestWaitForShutdown_Timeout(t *testing.T) {
// 跳过此测试:需要实际运行的进程来测试超时
// 向当前进程发送 SIGKILL 会导致测试崩溃
t.Skip("Skipping test that would kill current process")
}
// TestWaitForShutdown_SetsOldPid 测试 oldPid 设置
func TestWaitForShutdown_SetsOldPid(t *testing.T) {
mgr := NewUpgradeManager(nil)
// oldPid 为 0 时应该直接返回 nil
if mgr.oldPid != 0 {
t.Error("Expected oldPid to be 0 initially")
}
err := mgr.WaitForShutdown(100 * time.Millisecond)
if err != nil {
t.Errorf("Expected nil when oldPid is 0, got: %v", err)
}
}
// TestListenerFile_UnixListener 测试 Unix 监听器获取文件
// 注意:跳过此测试,因为在大量测试运行时可能导致 FD 问题
func TestListenerFile_UnixListener(t *testing.T) {
t.Skip("Skipping test to avoid FD exhaustion in parallel test runs")
}
// TestGracefulUpgrade_MultipleListeners 测试多个监听器的升级
// 注意:使用 mock 监听器避免 FD 问题
func TestGracefulUpgrade_MultipleListeners(t *testing.T) {
mgr := NewUpgradeManager(nil)
// 使用多个 mock 监听器
mgr.SetListeners([]net.Listener{&mockListener{}, &mockListener{}})
// 由于 mockListener 是不支持的类型,应该返回错误
err := mgr.GracefulUpgrade("/nonexistent/binary")
if err == nil {
t.Error("Expected error for unsupported listener type")
}
}
// TestGracefulUpgrade_RelativePath 测试相对路径的二进制文件
// 注意:使用 mock 监听器避免 FD 问题
func TestGracefulUpgrade_RelativePath(t *testing.T) {
mgr := NewUpgradeManager(nil)
// 使用 mock 监听器
mgr.SetListeners([]net.Listener{&mockListener{}})
// 使用相对路径,应该返回错误
err := mgr.GracefulUpgrade("./nonexistent")
if err == nil {
t.Error("Expected error for unsupported listener type")
}
}
// TestWaitForShutdown_FindProcessError 测试 FindProcess 行为
func TestWaitForShutdown_FindProcessError(t *testing.T) {
// 在 Unix 系统上os.FindProcess 总是成功
// 我们需要测试 Signal(0) 失败的情况
mgr := NewUpgradeManager(nil)
mgr.oldPid = 1 // init 进程,通常存在但无法发送信号
// 短超时
_ = mgr.WaitForShutdown(50 * time.Millisecond)
// 无论结果如何,测试都应该正常完成
}
// TestGetInheritedListeners_EnvPreserved 测试环境变量处理
func TestGetInheritedListeners_EnvPreserved(t *testing.T) {
t.Setenv("LISTEN_FDS", "1")
mgr := NewUpgradeManager(nil)
_, err := mgr.GetInheritedListeners()
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
// 环境变量应该仍然存在
if os.Getenv("LISTEN_FDS") != "1" {
t.Error("LISTEN_FDS env should be preserved")
}
}
// containsString 检查字符串是否包含子串
func containsString(s, substr string) bool {
for i := 0; i <= len(s)-len(substr); i++ {
if s[i:i+len(substr)] == substr {
return true
}
}
return false
}