使用空白标识符忽略测试辅助函数中 Close、ReadFrom、Set 等返回值, 与主代码风格保持一致。 Co-Authored-By: Claude <noreply@anthropic.com>
540 lines
12 KiB
Go
540 lines
12 KiB
Go
// Package app 提供应用程序的启动和运行逻辑。
|
||
package app
|
||
|
||
import (
|
||
"bytes"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
"syscall"
|
||
"testing"
|
||
|
||
"rua.plus/lolly/internal/config"
|
||
"rua.plus/lolly/internal/logging"
|
||
"rua.plus/lolly/internal/server"
|
||
)
|
||
|
||
// captureStdout 捕获 stdout 输出,返回捕获的内容和恢复函数。
|
||
func captureStdout(t *testing.T) (func() string, func()) {
|
||
t.Helper()
|
||
old := os.Stdout
|
||
r, w, err := os.Pipe()
|
||
if err != nil {
|
||
t.Fatalf("创建 pipe 失败: %v", err)
|
||
}
|
||
os.Stdout = w
|
||
|
||
return func() string {
|
||
_ = w.Close()
|
||
os.Stdout = old
|
||
var buf bytes.Buffer
|
||
_, _ = buf.ReadFrom(r)
|
||
return buf.String()
|
||
}, func() {
|
||
_ = w.Close()
|
||
os.Stdout = old
|
||
}
|
||
}
|
||
|
||
// captureStderr 捕获 stderr 输出,返回捕获的内容和恢复函数。
|
||
func captureStderr(t *testing.T) (func() string, func()) {
|
||
t.Helper()
|
||
old := os.Stderr
|
||
r, w, err := os.Pipe()
|
||
if err != nil {
|
||
t.Fatalf("创建 pipe 失败: %v", err)
|
||
}
|
||
os.Stderr = w
|
||
|
||
return func() string {
|
||
_ = w.Close()
|
||
os.Stderr = old
|
||
var buf bytes.Buffer
|
||
_, _ = buf.ReadFrom(r)
|
||
return buf.String()
|
||
}, func() {
|
||
_ = w.Close()
|
||
os.Stderr = old
|
||
}
|
||
}
|
||
|
||
// TestNewApp 测试 NewApp 构造器。
|
||
func TestNewApp(t *testing.T) {
|
||
cfgPath := "/path/to/config.yaml"
|
||
app := NewApp(cfgPath)
|
||
|
||
if app.cfgPath != cfgPath {
|
||
t.Errorf("cfgPath = %q, want %q", app.cfgPath, cfgPath)
|
||
}
|
||
|
||
if app.cfg != nil {
|
||
t.Error("新创建的 App cfg 应为 nil")
|
||
}
|
||
|
||
if app.srv != nil {
|
||
t.Error("新创建的 App srv 应为 nil")
|
||
}
|
||
|
||
if app.pidFile != "" {
|
||
t.Errorf("pidFile = %q, want empty", app.pidFile)
|
||
}
|
||
|
||
if app.logFile != "" {
|
||
t.Errorf("logFile = %q, want empty", app.logFile)
|
||
}
|
||
}
|
||
|
||
// TestSetPidFile 测试 SetPidFile setter 方法。
|
||
func TestSetPidFile(t *testing.T) {
|
||
app := NewApp("/test/config.yaml")
|
||
pidPath := "/var/run/lolly.pid"
|
||
|
||
app.SetPidFile(pidPath)
|
||
|
||
if app.pidFile != pidPath {
|
||
t.Errorf("pidFile = %q, want %q", app.pidFile, pidPath)
|
||
}
|
||
}
|
||
|
||
// TestSetLogFile 测试 SetLogFile setter 方法。
|
||
func TestSetLogFile(t *testing.T) {
|
||
app := NewApp("/test/config.yaml")
|
||
logPath := "/var/log/lolly.log"
|
||
|
||
app.SetLogFile(logPath)
|
||
|
||
if app.logFile != logPath {
|
||
t.Errorf("logFile = %q, want %q", app.logFile, logPath)
|
||
}
|
||
}
|
||
|
||
// TestSigName 测试信号名称辅助函数。
|
||
func TestSigName(t *testing.T) {
|
||
tests := []struct {
|
||
name string
|
||
sig syscall.Signal
|
||
expected string
|
||
}{
|
||
{
|
||
name: "SIGTERM",
|
||
sig: syscall.SIGTERM,
|
||
expected: "SIGTERM",
|
||
},
|
||
{
|
||
name: "SIGINT",
|
||
sig: syscall.SIGINT,
|
||
expected: "SIGINT",
|
||
},
|
||
{
|
||
name: "SIGQUIT",
|
||
sig: syscall.SIGQUIT,
|
||
expected: "SIGQUIT",
|
||
},
|
||
{
|
||
name: "SIGHUP",
|
||
sig: syscall.SIGHUP,
|
||
expected: "SIGHUP",
|
||
},
|
||
{
|
||
name: "SIGUSR1",
|
||
sig: syscall.SIGUSR1,
|
||
expected: "SIGUSR1",
|
||
},
|
||
{
|
||
name: "SIGUSR2",
|
||
sig: syscall.SIGUSR2,
|
||
expected: "SIGUSR2",
|
||
},
|
||
{
|
||
name: "未知信号",
|
||
sig: syscall.Signal(999),
|
||
expected: "Signal(999)",
|
||
},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
result := sigName(tt.sig)
|
||
if result != tt.expected {
|
||
t.Errorf("sigName(%d) = %q, want %q", tt.sig, result, tt.expected)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
func TestRun(t *testing.T) {
|
||
tests := []struct {
|
||
name string
|
||
cfgPath string
|
||
genConfig bool
|
||
outputPath string
|
||
showVersion bool
|
||
wantExitCode int
|
||
wantContains string // stdout 应包含的内容
|
||
wantErrContains string // stderr 应包含的内容(可选)
|
||
}{
|
||
{
|
||
name: "显示版本",
|
||
showVersion: true,
|
||
wantExitCode: 0,
|
||
wantContains: "lolly version",
|
||
},
|
||
{
|
||
name: "生成配置输出到 stdout",
|
||
genConfig: true,
|
||
outputPath: "",
|
||
wantExitCode: 0,
|
||
wantContains: "server:",
|
||
},
|
||
{
|
||
name: "生成配置输出到文件",
|
||
genConfig: true,
|
||
outputPath: filepath.Join(t.TempDir(), "config.yaml"),
|
||
wantExitCode: 0,
|
||
wantContains: "配置已写入:",
|
||
},
|
||
{
|
||
name: "配置文件不存在",
|
||
cfgPath: filepath.Join(t.TempDir(), "nonexistent.yaml"),
|
||
genConfig: false,
|
||
showVersion: false,
|
||
wantExitCode: 1,
|
||
wantErrContains: "加载配置失败",
|
||
},
|
||
}
|
||
|
||
for _, tt := range tests {
|
||
t.Run(tt.name, func(t *testing.T) {
|
||
getStdout, restoreStdout := captureStdout(t)
|
||
getStderr, restoreStderr := captureStderr(t)
|
||
|
||
exitCode := Run(tt.cfgPath, tt.genConfig, tt.outputPath, tt.showVersion)
|
||
|
||
restoreStderr()
|
||
restoreStdout()
|
||
|
||
stdout := getStdout()
|
||
stderr := getStderr()
|
||
|
||
if exitCode != tt.wantExitCode {
|
||
t.Errorf("exit code = %d, want %d", exitCode, tt.wantExitCode)
|
||
}
|
||
|
||
if tt.wantContains != "" && !strings.Contains(stdout, tt.wantContains) {
|
||
t.Errorf("stdout 应包含 %q, 实际输出: %q", tt.wantContains, stdout)
|
||
}
|
||
|
||
if tt.wantErrContains != "" && !strings.Contains(stderr, tt.wantErrContains) {
|
||
t.Errorf("stderr 应包含 %q, 实际输出: %q", tt.wantErrContains, stderr)
|
||
}
|
||
|
||
// 验证生成配置文件的内容
|
||
if tt.outputPath != "" && tt.genConfig && exitCode == 0 {
|
||
data, err := os.ReadFile(tt.outputPath)
|
||
if err != nil {
|
||
t.Errorf("读取生成的配置文件失败: %v", err)
|
||
} else if !strings.Contains(string(data), "server:") {
|
||
t.Errorf("生成的配置文件应包含 'server:', 实际内容: %s", string(data)[:100])
|
||
}
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// TestGenerateConfig 测试 generateConfig 函数。
|
||
func TestGenerateConfig(t *testing.T) {
|
||
t.Run("输出到 stdout", func(t *testing.T) {
|
||
getStdout, restoreStdout := captureStdout(t)
|
||
|
||
exitCode := generateConfig("")
|
||
restoreStdout()
|
||
|
||
stdout := getStdout()
|
||
|
||
if exitCode != 0 {
|
||
t.Errorf("exit code = %d, want 0", exitCode)
|
||
}
|
||
|
||
// 验证输出包含基本配置结构
|
||
expectedFields := []string{"server:", "listen:", "logging:", "performance:", "monitoring:"}
|
||
for _, field := range expectedFields {
|
||
if !strings.Contains(stdout, field) {
|
||
t.Errorf("输出应包含 %q", field)
|
||
}
|
||
}
|
||
})
|
||
|
||
t.Run("输出到文件", func(t *testing.T) {
|
||
tmpDir := t.TempDir()
|
||
outputPath := filepath.Join(tmpDir, "test-config.yaml")
|
||
|
||
getStdout, restoreStdout := captureStdout(t)
|
||
|
||
exitCode := generateConfig(outputPath)
|
||
restoreStdout()
|
||
|
||
stdout := getStdout()
|
||
|
||
if exitCode != 0 {
|
||
t.Errorf("exit code = %d, want 0", exitCode)
|
||
}
|
||
|
||
if !strings.Contains(stdout, outputPath) {
|
||
t.Errorf("stdout 应包含文件路径 %q, 实际输出: %q", outputPath, stdout)
|
||
}
|
||
|
||
// 验证文件存在且内容正确
|
||
data, err := os.ReadFile(outputPath)
|
||
if err != nil {
|
||
t.Fatalf("读取生成的配置文件失败: %v", err)
|
||
}
|
||
|
||
content := string(data)
|
||
expectedFields := []string{"server:", "listen:", "logging:", "performance:", "monitoring:"}
|
||
for _, field := range expectedFields {
|
||
if !strings.Contains(content, field) {
|
||
t.Errorf("配置文件应包含 %q", field)
|
||
}
|
||
}
|
||
})
|
||
|
||
t.Run("输出到无效路径", func(t *testing.T) {
|
||
// 使用一个无法写入的路径(如根目录下的文件)
|
||
invalidPath := "/root/cannot-write-here.yaml"
|
||
|
||
getStderr, restoreStderr := captureStderr(t)
|
||
|
||
exitCode := generateConfig(invalidPath)
|
||
restoreStderr()
|
||
|
||
stderr := getStderr()
|
||
|
||
if exitCode != 1 {
|
||
t.Errorf("exit code = %d, want 1", exitCode)
|
||
}
|
||
|
||
if !strings.Contains(stderr, "写入文件失败") {
|
||
t.Errorf("stderr 应包含 '写入文件失败', 实际输出: %q", stderr)
|
||
}
|
||
})
|
||
}
|
||
|
||
// TestPrintVersion 测试 printVersion 函数。
|
||
func TestPrintVersion(t *testing.T) {
|
||
getStdout, restoreStdout := captureStdout(t)
|
||
|
||
printVersion()
|
||
restoreStdout()
|
||
|
||
stdout := getStdout()
|
||
|
||
// 验证版本输出格式
|
||
expectedLines := []string{
|
||
"lolly version",
|
||
"Git:",
|
||
"Built:",
|
||
"Go:",
|
||
"Platform:",
|
||
}
|
||
|
||
for _, line := range expectedLines {
|
||
if !strings.Contains(stdout, line) {
|
||
t.Errorf("版本输出应包含 %q, 实际输出: %q", line, stdout)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 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)
|
||
}
|
||
}
|