lolly/internal/app/app_test.go
xfy c70ab305b7 test(app,proxy,ssl,stream): 完善测试覆盖率
- app: 添加 NewApp/SetPidFile/SetLogFile/sigName 测试
- proxy: 扩展健康检查器测试
- ssl: 添加 TLS 配置和 Close 方法测试
- stream: 添加负载均衡器和 UDP 会话测试

覆盖率从 55.4% 提升至 60.3%

Co-Authored-By: Claude <noreply@anthropic.com>
2026-04-03 13:36:43 +08:00

341 lines
7.7 KiB
Go

// Package app 提供应用程序的启动和运行逻辑。
package app
import (
"bytes"
"os"
"path/filepath"
"strings"
"syscall"
"testing"
)
// 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)
}
}
}