package main import ( "fmt" "path/filepath" "strings" "testing" ) // 按最坏情况(每条弹幕一个不同用户、每个都带头像、文字宽度顶满整屏)渲染一块, // 返回这一块的命令行长度。长度从 renderChunk 自己的自检错误里读出来; // 没触发自检就返回 (0, false)。 // // 头像路径必须按 renderChunk 真正会拿到的形态造 —— 预缩放目录里的短相对路径 // (见 prepareAvatarInputs),不是 avatar_cache 里的长绝对路径;用后者量出来的是 // 一个早就不存在的旧版长度,会把上限算得过分保守。 // // 线上的那次事故就是这里没守住:单块 200 条时实测 48288 字符,远超 Windows 的 32767, // 而 exec 只回一句 "The filename or extension is too long",看不出跟弹幕数量有关。 func chunkCommandLenProbe(t *testing.T, n int) (length int, over bool) { t.Helper() dir := t.TempDir() avatars := make(map[int64]string, n) records := make([]danmuRecord, 0, n) for i := 0; i < n; i++ { uid := int64(1000000 + i) avatars[uid] = chunkAvatarDir + "/" + fmt.Sprintf("%d.png", i+1) records = append(records, danmuRecord{ Time: float64(i) * 0.05, UID: uid, Name: "观众甲", Content: "这条弹幕稍微长一点测试宽度", Lane: i % 100, Width: 1920, }) } ctx := &burnContext{ ffmpegFile: filepath.Join(dir, "没有这个-ffmpeg.exe"), // 自检在 exec 之前,跑不到它 outFile: filepath.Join(dir, "out.mp4"), meta: danmuMeta{PlayResX: 1920, PlayResY: 1080, FontSize: 42}, avatars: avatars, dir: dir, } err := ctx.renderChunk(danmuChunk{start: 0, end: 60, records: records}, "chunk.ass", ctx.outFile, false, nil) if err == nil { t.Fatal("用不存在的 ffmpeg 路径不该成功") } if !strings.Contains(err.Error(), "命令过长") { return 0, false } if _, scanErr := fmt.Sscanf(err.Error(), "ffmpeg 命令过长(%d 字符", &length); scanErr != nil { t.Fatalf("没能从错误里读出长度:%v / %s", scanErr, err) } return length, true } // 按硬上限切出来的块,在最坏情况下也不能顶破命令行上限 func TestChunkCommandLengthUnderLimit(t *testing.T) { if length, over := chunkCommandLenProbe(t, maxDanmuPerChunkHard); over { t.Fatalf("按硬上限 %d 条渲染,命令长达 %d 字符,已经超过安全值 %d", maxDanmuPerChunkHard, length, maxCommandLineLen) } } // 块大到一定程度必须被自检拦住,并报出「多少条弹幕、多少个头像」这种能定位的信息 func TestRenderChunkReportsTooLongCommand(t *testing.T) { length, over := chunkCommandLenProbe(t, 300) if !over { t.Fatal("300 条的最坏情况应当被自检拦住,否则交给 exec 只会报一句看不出原因的错") } if length <= maxCommandLineLen { t.Fatalf("自检报出的长度 %d 应当大于阈值 %d", length, maxCommandLineLen) } } // 硬上限本身要留出余量:真跑一遍「上限条数 + 最坏情况」,而不是拿一个会过期的 // 单条估算值乘一乘。换了头像方案(绝对路径 → 预缩放短路径)、滤镜写法或 // 单条 overlay 的表达式以后,这个用例会跟着一起变,不会悄悄失效 func TestChunkHardLimitLeavesHeadroom(t *testing.T) { if length, over := chunkCommandLenProbe(t, maxDanmuPerChunkHard); over { t.Fatalf("硬上限 %d 条的最坏情况实测 %d 字符,已经超过安全值 %d", maxDanmuPerChunkHard, length, maxCommandLineLen) } // 再往上加一成也不该顶破安全值,防止上限被悄悄调高到刚好卡线 headroom := maxDanmuPerChunkHard / 10 if length, over := chunkCommandLenProbe(t, maxDanmuPerChunkHard+headroom); over { t.Fatalf("上限再高 %d 条(共 %d 条)就顶破了安全值 %d 字符,余量太小", headroom, maxDanmuPerChunkHard+headroom, length) } }