package main import ( "encoding/json" "os" "path/filepath" "strings" "testing" ) // 「合并失败」推送是新增的开关,老设置文件里没有这个字段 —— 升级后不能静默关掉, // 也不能把用户显式写的 false 顶回 true。同一个坑「推送渠道」已经踩过一次 func TestLoadConfigPresetsNotifyMergeFail(t *testing.T) { dir := t.TempDir() savedConfig := config savedLocation := configFileLocation oldDir := configDir if configDir == nil { configDir = new(string) } defer func() { config = savedConfig configFileLocation = savedLocation configDir = oldDir }() *configDir = dir configFileLocation = filepath.Join(dir, configFile) // 老文件:连 notifyEnabled 都没有,两个开关都该是默认开 content, _ := json.Marshal(map[string]any{"uids": []any{}, "mergeFps": 60}) if err := os.WriteFile(configFileLocation, content, 0644); err != nil { t.Fatal(err) } config.NotifyMergeFail = false // 先弄脏,确认是 Unmarshal 之前被顶回 true 的 loadConfig() if !config.NotifyMergeFail { t.Error("老设置文件里没有 notifyMergeFail 时应当按开启处理") } if config.MergeFps != 60 { t.Errorf("mergeFps 没读进来:%d", config.MergeFps) } // 文件里显式写了 false,必须尊重 content, _ = json.Marshal(map[string]any{"notifyMergeFail": false, "mergeFps": 0}) if err := os.WriteFile(configFileLocation, content, 0644); err != nil { t.Fatal(err) } config.NotifyMergeFail = true loadConfig() if config.NotifyMergeFail { t.Error("文件里显式写的 notifyMergeFail=false 被顶回了 true") } if config.MergeFps != 0 { t.Errorf("mergeFps 应当是 0,实际 %d", config.MergeFps) } } // 命令行合成模式(-danmuburn)不走 initialize(),exeDir 必须由 loadConfigForDanmuburn 自己定: // 漏掉的话头像缓存就变成「当前工作目录/avatar_cache」—— 换个目录跑要重新下一遍头像, // 更糟的是旧版把头像一个相对路径写进滤镜,而 ffmpeg 的工作目录是弹幕目录, // 于是找不到文件、整块渲染失败(报错只显示「打不开 avatar_cache\xxx.png」) func TestLoadConfigForDanmuburnSetsExeDir(t *testing.T) { savedConfig := config savedExe, savedLocation := exeDir, configFileLocation oldDir, oldRecordDir := configDir, recordDir if configDir == nil { configDir = new(string) } if recordDir == nil { recordDir = new(string) } defer func() { config = savedConfig exeDir, configFileLocation = savedExe, savedLocation configDir, recordDir = oldDir, oldRecordDir }() exeDir = "" *configDir = "" loadConfigForDanmuburn() if exeDir == "" { t.Fatal("loadConfigForDanmuburn 应当把 exeDir 定下来,否则头像缓存会落到当前工作目录") } if !filepath.IsAbs(avatarCachePath()) { t.Fatalf("头像缓存路径应当是绝对路径,实际 %q", avatarCachePath()) } if filepath.Dir(configFileLocation) != exeDir { t.Errorf("没指定 -config 时设置文件应当在程序目录,实际 %q(exeDir=%q)", configFileLocation, exeDir) } } // 改设置会重新读取配置(web UI 保存、或直接改 config.json), // 这一步绝不能删掉正在进行的合并的临时文件 —— 否则合并会当场失败。 func TestReloadConfigKeepsMergeTempFiles(t *testing.T) { dir := t.TempDir() savedConfig := config savedLocation := configFileLocation oldDir := configDir if configDir == nil { configDir = new(string) } defer func() { config = savedConfig configFileLocation = savedLocation configDir = oldDir }() *configDir = dir configFileLocation = filepath.Join(dir, configFile) recDir := filepath.Join(dir, defaultRecordSubdir) if err := os.MkdirAll(recDir, 0755); err != nil { t.Fatal(err) } // 假装一个合并正在跑:录播目录里有分块字幕和先本地合成的成片 inUse := []string{ filepath.Join(recDir, chunkTempPrefix+"single.ass"), filepath.Join(recDir, chunkTempPrefix+"merging_x.mp4"), } for _, p := range inUse { if err := os.WriteFile(p, []byte("x"), 0644); err != nil { t.Fatal(err) } } content, _ := json.Marshal(map[string]string{"directory": filepath.ToSlash(dir)}) if err := os.WriteFile(configFileLocation, content, 0644); err != nil { t.Fatal(err) } // 重新读取配置的真实路径:loadNewConfig() 就是 loadConfig() + checkConfig() loadConfig() checkConfig() for _, p := range inUse { if _, err := os.Stat(p); err != nil { t.Errorf("重新读取配置把正在用的临时文件删了:%s", filepath.Base(p)) } } // 启动时的兜底清理还得照旧能清掉上次残留 cleanLeftoverTempFiles() for _, p := range inUse { if _, err := os.Stat(p); err == nil { t.Errorf("启动时的清理没清掉残留:%s", filepath.Base(p)) } } } // 保存目录填错(盘没插、路径拼错)时启动必须给一句人话再退出, // 不能 panic 崩掉整个进程 —— 服务被托管时崩了还会被反复拉起来 func TestCheckDirFieldReportsMissingDir(t *testing.T) { root := t.TempDir() if msg := checkDirField("", "directory"); msg != "" { t.Errorf("留空表示不指定,不该报错:%s", msg) } if msg := checkDirField(root, "directory"); msg != "" { t.Errorf("存在的文件夹不该报错:%s", msg) } if msg := checkDirField(filepath.Join(root, "没有这个目录"), "directory"); !strings.Contains(msg, "必须是存在的文件夹") { t.Errorf("目录不存在时要给出提示,实际返回 %q", msg) } file := filepath.Join(root, "afile") if err := os.WriteFile(file, []byte("x"), 0644); err != nil { t.Fatal(err) } if msg := checkDirField(file, "directory"); msg == "" { t.Error("指向文件而不是文件夹时应当报错") } } // checkConfig 里曾经直接 os.Stat + checkErr:目录不在时会 panic 崩掉进程。 // 这条路径没法在测试里真跑一遍(失败时是 os.Exit(1),会把整个测试进程带走), // 所以退一步把「只能给提示再退出」钉在源码上。 func TestCheckConfigNeverPanicsOnMissingDir(t *testing.T) { src, err := os.ReadFile("config.go") if err != nil { t.Skip("读不到 config.go:" + err.Error()) } text := string(src) start := strings.Index(text, "func checkConfig()") if start < 0 { t.Fatal("config.go 里找不到 checkConfig") } rest := text[start:] end := strings.Index(rest, "\n}\n") if end < 0 { t.Fatal("checkConfig 的函数体没有正常收尾") } if strings.Contains(rest[:end], "checkErr(") { t.Error("checkConfig 里不能用 checkErr:出错会变成 panic 崩进程,改成 lPrintErr + os.Exit(1)") } }