- app: 添加 NewApp/SetPidFile/SetLogFile/sigName 测试 - proxy: 扩展健康检查器测试 - ssl: 添加 TLS 配置和 Close 方法测试 - stream: 添加负载均衡器和 UDP 会话测试 覆盖率从 55.4% 提升至 60.3% Co-Authored-By: Claude <noreply@anthropic.com>
341 lines
7.7 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|