Files
tamakyi 2b1705a970 保存目录不存在时给一句人话再退出,不再 panic 崩进程
为什么:

checkConfig() 里检查 directory / mergeOutputDir 用的是 os.Stat + checkErr(err)。
目录不在时(盘没插、路径拼错)前者失败会直接 panic,打印一屏 goroutine 堆栈后崩掉
整个进程,而不是像同函数里其它检查那样给一句提示再 os.Exit(1)。服务被 nssm 之类托管
时崩了还会被反复拉起来,用户看到的只是一堆堆栈。

处理方式是复用原本只给 web 用的 checkWebDir,把它提升成通用的 checkDirField 并搬到
config.go:启动读配置、web 改设置、转码排队从此共用同一份实现,不会有两份各自漂移的
写法。文案沿用原来 IsDir 分支的措辞(「X 必须是存在的文件夹:路径」),只是不再依赖
os.Stat 先成功,且空串仍然表示「不指定」而不是错误。

验证:

- 实跑对比。修前 directory 指向不存在的目录:
  panic: GetFileAttributesEx ...: The system cannot find the file specified.
  修后:[ERROR] config.json里的directory 必须是存在的文件夹:… 退出码 1,
  日志里 panic 出现 0 次。
- 同一个 exe 另验两件事:directory 指向存在的目录能正常启动并建出 record/merged;
  web /api/config 传不存在的目录仍返回 400 + 同一套文案(改名没走样)。
- 新增 2 条测试,其中 TestCheckConfigNeverPanicsOnMissingDir 是静态检查——这条失败
  路径是 os.Exit(1),真跑一遍会把整个测试进程带走,所以改为读 config.go 源码断言
  checkConfig 函数体里不出现 checkErr(,与项目里既有的静态一致性检查同一套路。
- 反向验证新增一项:把两处换回老的 checkErr 写法后,该测试确实变红。
2026-09-28 02:03:56 +08:00

194 lines
6.5 KiB
Go
Raw Permalink 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 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)")
}
}