|
@@ -3,6 +3,7 @@ package main
|
|
|
import (
|
|
import (
|
|
|
"net"
|
|
"net"
|
|
|
"os"
|
|
"os"
|
|
|
|
|
+ "path/filepath"
|
|
|
"strings"
|
|
"strings"
|
|
|
"testing"
|
|
"testing"
|
|
|
"time"
|
|
"time"
|
|
@@ -109,6 +110,125 @@ func TestIsPortOpen(t *testing.T) {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// TestConfigRoundTrip 覆盖配置的首次生成、保存重载、BOM 容忍与损坏回退。
|
|
|
|
|
+// 通过 dataDirForced 注入临时目录,避免触碰真实用户配置。
|
|
|
|
|
+func TestConfigRoundTrip(t *testing.T) {
|
|
|
|
|
+ dataDirForced = t.TempDir()
|
|
|
|
|
+ defer func() { dataDirForced = "" }()
|
|
|
|
|
+
|
|
|
|
|
+ // 首次加载:生成默认配置
|
|
|
|
|
+ c := loadConfig()
|
|
|
|
|
+ if c.WebPort != 3080 || c.AutoUpdate || c.RestartDelaySec != 5 {
|
|
|
|
|
+ t.Fatalf("默认配置异常: %+v", c)
|
|
|
|
|
+ }
|
|
|
|
|
+ if _, err := os.Stat(configPath()); err != nil {
|
|
|
|
|
+ t.Errorf("首次加载应生成配置文件: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 保存后重载
|
|
|
|
|
+ c.WebPort = 3099
|
|
|
|
|
+ c.AutoUpdate = true
|
|
|
|
|
+ if err := saveConfig(c); err != nil {
|
|
|
|
|
+ t.Fatalf("保存失败: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ c2 := loadConfig()
|
|
|
|
|
+ if c2.WebPort != 3099 || !c2.AutoUpdate {
|
|
|
|
|
+ t.Errorf("重载不一致: %+v", c2)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // BOM 容忍
|
|
|
|
|
+ bom := append([]byte{0xEF, 0xBB, 0xBF}, []byte(`{"webPort":1234}`)...)
|
|
|
|
|
+ if err := os.WriteFile(configPath(), bom, 0o600); err != nil {
|
|
|
|
|
+ t.Fatal(err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if got := loadConfig().WebPort; got != 1234 {
|
|
|
|
|
+ t.Errorf("BOM 配置应解析为 1234,得到 %d", got)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 损坏配置回退默认值
|
|
|
|
|
+ if err := os.WriteFile(configPath(), []byte(`{ broken`), 0o600); err != nil {
|
|
|
|
|
+ t.Fatal(err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if got := loadConfig().WebPort; got != 3080 {
|
|
|
|
|
+ t.Errorf("损坏配置应回退 3080,得到 %d", got)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// TestSupervisorLifecycle 用"立即退出的假命令"验证守护状态机:启动、自动重启、停止。
|
|
|
|
|
+func TestSupervisorLifecycle(t *testing.T) {
|
|
|
|
|
+ cmdExe := os.Getenv("COMSPEC")
|
|
|
|
|
+ if cmdExe == "" {
|
|
|
|
|
+ t.Skip("缺少 COMSPEC,跳过守护生命周期测试")
|
|
|
|
|
+ }
|
|
|
|
|
+ dir := t.TempDir()
|
|
|
|
|
+ dataDirForced = dir
|
|
|
|
|
+ defer func() { dataDirForced = "" }()
|
|
|
|
|
+
|
|
|
|
|
+ // 假 dsh:立即退出(触发守护重启)
|
|
|
|
|
+ bat := filepath.Join(dir, "fake-dsh.bat")
|
|
|
|
|
+ if err := os.WriteFile(bat, []byte("@echo off\r\nexit /b 1\r\n"), 0o600); err != nil {
|
|
|
|
|
+ t.Fatal(err)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ cfg := defaultConfig()
|
|
|
|
|
+ cfg.NodePath = cmdExe
|
|
|
|
|
+ cfg.DshBinJS = bat
|
|
|
|
|
+ cfg.WebPort = 1 // 无监听端口,避免误判"端口被占用"
|
|
|
|
|
+ cfg.RestartDelaySec = 1
|
|
|
|
|
+
|
|
|
|
|
+ log := NewLogger()
|
|
|
|
|
+ defer log.Close()
|
|
|
|
|
+ s := NewSupervisor(cfg, log)
|
|
|
|
|
+
|
|
|
|
|
+ s.Start()
|
|
|
|
|
+ if !s.isDesired() {
|
|
|
|
|
+ t.Errorf("Start 后 desired 应为 true")
|
|
|
|
|
+ }
|
|
|
|
|
+ time.Sleep(2500 * time.Millisecond)
|
|
|
|
|
+ if got := s.Status().Restarts; got < 1 {
|
|
|
|
|
+ t.Errorf("假命令立即退出应至少触发一次自动重启,得到 %d", got)
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ s.Stop()
|
|
|
|
|
+ if s.isDesired() {
|
|
|
|
|
+ t.Errorf("Stop 后 desired 应为 false")
|
|
|
|
|
+ }
|
|
|
|
|
+ time.Sleep(600 * time.Millisecond)
|
|
|
|
|
+ if s.Status().Running {
|
|
|
|
|
+ t.Errorf("Stop 后不应处于运行态")
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// TestSetAutoUpdatePersists 验证线程安全 setter 同时更新内存与磁盘。
|
|
|
|
|
+func TestSetAutoUpdatePersists(t *testing.T) {
|
|
|
|
|
+ dataDirForced = t.TempDir()
|
|
|
|
|
+ defer func() { dataDirForced = "" }()
|
|
|
|
|
+
|
|
|
|
|
+ log := NewLogger()
|
|
|
|
|
+ defer log.Close()
|
|
|
|
|
+ s := NewSupervisor(loadConfig(), log)
|
|
|
|
|
+
|
|
|
|
|
+ if err := s.SetAutoUpdate(true); err != nil {
|
|
|
|
|
+ t.Fatalf("SetAutoUpdate 失败: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if !loadConfig().AutoUpdate {
|
|
|
|
|
+ t.Errorf("配置未持久化到磁盘")
|
|
|
|
|
+ }
|
|
|
|
|
+ s.mu.Lock()
|
|
|
|
|
+ inMemory := s.cfg.AutoUpdate
|
|
|
|
|
+ s.mu.Unlock()
|
|
|
|
|
+ if !inMemory {
|
|
|
|
|
+ t.Errorf("内存配置未更新")
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if err := s.SetAutoUpdate(false); err != nil {
|
|
|
|
|
+ t.Fatalf("SetAutoUpdate(false) 失败: %v", err)
|
|
|
|
|
+ }
|
|
|
|
|
+ if loadConfig().AutoUpdate {
|
|
|
|
|
+ t.Errorf("关闭后配置仍为 true")
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
// TestLoggerRotate 验证日志按天轮转(切换文件),并清理测试产物。
|
|
// TestLoggerRotate 验证日志按天轮转(切换文件),并清理测试产物。
|
|
|
func TestLoggerRotate(t *testing.T) {
|
|
func TestLoggerRotate(t *testing.T) {
|
|
|
l := NewLogger()
|
|
l := NewLogger()
|