lolly/internal/app/app_test.go
xfy fc71cf4835 refactor(test): 统一测试文件错误处理风格
使用空白标识符忽略测试辅助函数中 Close、ReadFrom、Set 等返回值,
与主代码风格保持一致。

Co-Authored-By: Claude <noreply@anthropic.com>
2026-04-03 17:37:05 +08:00

540 lines
12 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.

// 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)
}
}