为什么: 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 写法后,该测试确实变红。
194 lines
6.5 KiB
Go
194 lines
6.5 KiB
Go
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)")
|
||
}
|
||
}
|