package main import ( "bytes" "crypto/sha1" "encoding/base64" "fmt" "image" "image/color" "image/gif" "image/jpeg" "image/png" "os" "path/filepath" "testing" ) // 2x2 的静态 webp(纯红),用来确认 webp 解码器确实注册上了 const tinyWebPBase64 = "UklGRjwAAABXRUJQVlA4IDAAAADQAQCdASoCAAIAAgA0JaACdLoB+AADsAD+8Oj3/yC5YXXI1/8gP+QH/ID/+PIAAAA=" // 多帧 gif 解出来应该是第一帧(AcFun 上有动图头像,压成静态就用第一帧) func TestDecodeAvatarGifTakesFirstFrame(t *testing.T) { pal := color.Palette{color.RGBA{R: 255, A: 255}, color.RGBA{B: 255, A: 255}} first := image.NewPaletted(image.Rect(0, 0, 8, 8), pal) second := image.NewPaletted(image.Rect(0, 0, 8, 8), pal) for i := range second.Pix { second.Pix[i] = 1 } var buf bytes.Buffer if err := gif.EncodeAll(&buf, &gif.GIF{ Image: []*image.Paletted{first, second}, Delay: []int{10, 10}, }); err != nil { t.Fatal(err) } src, err := decodeAvatar(buf.Bytes(), "image/gif") if err != nil { t.Fatalf("gif 解码失败:%v", err) } r, g, b, _ := src.At(4, 4).RGBA() if r>>8 != 255 || g>>8 != 0 || b>>8 != 0 { t.Errorf("应该取第一帧(红),实际拿到 r=%d g=%d b=%d", r>>8, g>>8, b>>8) } } // jpg / png / 静态 webp 都能解,解不出来的要给中文原因 func TestDecodeAvatarFormats(t *testing.T) { var pngBuf, jpgBuf bytes.Buffer img := image.NewRGBA(image.Rect(0, 0, 8, 8)) if err := png.Encode(&pngBuf, img); err != nil { t.Fatal(err) } if err := jpeg.Encode(&jpgBuf, img, nil); err != nil { t.Fatal(err) } webpData, err := base64.StdEncoding.DecodeString(tinyWebPBase64) if err != nil { t.Fatal(err) } cases := []struct { name string data []byte contentType string wantErr bool }{ {"png", pngBuf.Bytes(), "image/png", false}, {"jpeg", jpgBuf.Bytes(), "image/jpeg", false}, {"webp", webpData, "image/webp", false}, {"不是图片", []byte("这不是图片"), "text/html", true}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { src, err := decodeAvatar(c.data, c.contentType) if c.wantErr { if err == nil { t.Fatal("应该报错") } if !bytes.Contains([]byte(err.Error()), []byte(c.contentType)) { t.Errorf("错误信息里应该带上 Content-Type 方便排查:%v", err) } return } if err != nil { t.Fatalf("解码失败:%v", err) } if src == nil { t.Fatal("解出来是空的") } }) } } // 解出来的头像要能走通圆形裁切 + 缓存用的 PNG 编码(gif 是 *image.Paletted,和 jpg/png 的底层类型都不同) func TestDecodeAvatarGifGoesThroughCircleAvatar(t *testing.T) { pal := color.Palette{color.RGBA{G: 255, A: 255}, color.RGBA{R: 255, B: 255, A: 255}} frame := image.NewPaletted(image.Rect(0, 0, 16, 16), pal) var buf bytes.Buffer if err := gif.EncodeAll(&buf, &gif.GIF{Image: []*image.Paletted{frame}, Delay: []int{10}}); err != nil { t.Fatal(err) } src, err := decodeAvatar(buf.Bytes(), "image/gif") if err != nil { t.Fatal(err) } var out bytes.Buffer if err := png.Encode(&out, circleAvatar(src, avatarCacheSize)); err != nil { t.Fatalf("圆形裁切后的 PNG 编码失败:%v", err) } if out.Len() == 0 { t.Fatal("编码出来是空的") } } // 头像缓存在程序所在目录,不再跟着保存目录跑 func TestAvatarCachePathUnderExeDir(t *testing.T) { old := exeDir defer func() { exeDir = old }() exeDir = filepath.Join("C:", "app", "acfunlive") want := filepath.Join("C:", "app", "acfunlive", avatarCacheDir) if got := avatarCachePath(); got != want { t.Errorf("应缓存到 %q,实际 %q", want, got) } // 取不到程序目录时退回相对路径,别拼出 /avatar_cache 这种绝对路径 exeDir = "" if got := avatarCachePath(); got != avatarCacheDir { t.Errorf("没有程序目录时应退回 %q,实际 %q", avatarCacheDir, got) } } // 旧版本把头像存在录播目录里,换保存目录后要能直接搬过来用,不重新下载 func TestEnsureAvatarMigratesLegacyCache(t *testing.T) { root := t.TempDir() cacheDir := filepath.Join(root, "program", avatarCacheDir) legacyDir := filepath.Join(root, "record", avatarCacheDir) if err := os.MkdirAll(legacyDir, 0755); err != nil { t.Fatal(err) } // 这个地址连不通:命中旧缓存就不该去下载(下载会重试 3 次,很慢) url := "http://127.0.0.1:9/never.png" sum := sha1.Sum([]byte(url)) name := fmt.Sprintf("%d_%x.png", int64(7), sum[:6]) old := filepath.Join(legacyDir, name) if err := os.WriteFile(old, []byte("cached-png"), 0644); err != nil { t.Fatal(err) } got := ensureAvatar(7, url, cacheDir, legacyDir) want := filepath.Join(cacheDir, name) if got != want { t.Fatalf("应搬到 %q,实际 %q", want, got) } data, err := os.ReadFile(want) if err != nil || string(data) != "cached-png" { t.Fatalf("搬过来的内容不对:%v %q", err, data) } if _, err := os.Stat(old); !os.IsNotExist(err) { t.Errorf("旧位置的文件应被搬走,实际还在:%v", err) } // 已经在程序目录里的直接命中,不用管旧目录 if got := ensureAvatar(7, url, cacheDir, legacyDir); got != want { t.Errorf("命中缓存应返回 %q,实际 %q", want, got) } }