package storage import ( "bytes" "image" "image/color" "image/gif" "testing" ) // buildGIF 生成指定帧数的动画 GIF。 func buildGIF(t *testing.T, frames int) []byte { t.Helper() g := &gif.GIF{} for i := 0; i < frames; i++ { pal := color.Palette{color.Black, color.White} img := image.NewPaletted(image.Rect(0, 0, 4, 4), pal) if i%2 == 1 { for y := 0; y < 4; y++ { for x := 0; x < 4; x++ { img.SetColorIndex(x, y, 1) } } } g.Image = append(g.Image, img) g.Delay = append(g.Delay, 10) } var b bytes.Buffer if err := gif.EncodeAll(&b, g); err != nil { t.Fatalf("encode gif: %v", err) } return b.Bytes() } func TestGIFFrameCount(t *testing.T) { cases := []struct { name string frames int }{ {"单帧", 1}, {"多帧", 12}, {"恰好等于上限", maxGIFFrames}, {"超过上限", maxGIFFrames + 1}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { data := buildGIF(t, c.frames) if got := gifFrameCount(data); got != c.frames { t.Fatalf("gifFrameCount = %d, want %d", got, c.frames) } }) } } // 帧数统计必须能承受构造超大 GIF 的场景而不过度消耗内存。 func TestGIFFrameCount_RejectsGarbage(t *testing.T) { cases := map[string][]byte{ "空数据": nil, "非 GIF": []byte("not a gif at all, just some bytes"), "仅有文件头": []byte("GIF89a"), "截断的 LSD": []byte("GIF89a\x01\x00"), "截断的色表": append([]byte("GIF89a"), bytes.Repeat([]byte{0xFF}, 8)...), } for name, data := range cases { t.Run(name, func(t *testing.T) { if n := gifFrameCount(data); n < 0 || n > 3 { t.Fatalf("gifFrameCount = %d, out of sane range", n) } }) } } // 转换入口在帧数超限时必须返回错误(而不是把超大 GIF 丢给 cwebp)。 // 注意本机无 cwebp 时该测试只能覆盖「帧数检查发生在 LookPath 之后」这条路径, // 故直接针对判定逻辑断言,不依赖外部二进制。 func TestGIFFrameLimitConstant(t *testing.T) { if maxGIFFrames <= 0 { t.Fatalf("maxGIFFrames = %d, must be positive", maxGIFFrames) } if gifFrameCount(buildGIF(t, maxGIFFrames+1)) <= maxGIFFrames { t.Fatal("oversized GIF should be detected as over limit") } if gifFrameCount(buildGIF(t, 3)) > maxGIFFrames { t.Fatal("small GIF should not be flagged as over limit") } }