Files
eh-downloader/internal/core/manifest_test.go
T
toor 0447875b6d feat: 双层并发 + 全局限流 + 整轮扫尾 + 画廊目录清单
原实现只能「多画廊并行,每个画廊串行下」,单张大画廊下得很慢。
按用户多年使用习惯(5 画廊 × 每画廊 8 张)改造为双层并发。

- 双层并发:画廊并发(默认5) × 页并发(默认8) = 最多 40 张同时下载,
  两个值都在设置面板可调(1-20)。页级 worker 池 + mutex 保护计数。
- 全局限流:对齐参考实现的 nextRequestTime 算法,限所有请求
  (含 /s/ 解析与图片本体),默认 0 秒关闭。
- 整轮扫尾:跑完后自动重扫失败页 1 轮(同样走完整 4 次退避重试),
  仍失败则停下并标记 partial,换线路后手动点开始即可续传。
- 画廊目录清单 .ehdl.json:记录 gid/token/总页数/已完成页,
  即使 state.json 丢失也能算出「应该多少张、还差多少张」。

顺带修复:
- Start() 双重加锁导致死锁(引擎完全无法启动)
- clampInt 越界时返回默认值而非边界值(设 30 会静默变 8)
- 直链正则写死 hath.network,CDN 换域名会全军覆没 -> 加图片 URL 兜底
- ReconcileTask 会误报「有改动」

测试从 37 增至 62,其中并发/限流/扫尾用 httptest 模拟 EH 实测:
- 页并发峰值 = 8、画廊并发峰值 = 5(完全生效)
- 12 次请求限流 80ms 实测 966ms(期望 880ms)
- 断点续传 4 张已存在时只发 5 次请求
2026-10-09 00:11:38 +08:00

241 lines
5.9 KiB
Go

package core
// 画廊清单(.ehdl.json)测试
import (
"os"
"path/filepath"
"testing"
"eh-downloader-wails/internal/eh"
)
func TestManifestRoundTrip(t *testing.T) {
dir := t.TempDir()
m := Manifest{
GID: "4239274",
Token: "55e069b6b9",
Title: "[Noxe] Test",
TotalPages: 206,
DonePages: []int{3, 1, 2},
FailedPage: []int{10, 9},
}
if err := WriteManifest(dir, m); err != nil {
t.Fatalf("写清单失败: %v", err)
}
// 文件应该真的存在
p := filepath.Join(dir, ManifestFileName)
if _, err := os.Stat(p); err != nil {
t.Fatalf("清单文件没生成: %v", err)
}
// 不该残留 .tmp
if _, err := os.Stat(p + ".tmp"); err == nil {
t.Error("临时文件没清理")
}
got, err := ReadManifest(dir)
if err != nil {
t.Fatalf("读清单失败: %v", err)
}
if got == nil {
t.Fatal("读回来的清单是 nil")
}
if got.GID != m.GID || got.Token != m.Token {
t.Errorf("gid/token 不对: %s/%s", got.GID, got.Token)
}
if got.TotalPages != 206 {
t.Errorf("总页数不对: %d", got.TotalPages)
}
if got.Version != ManifestVersion {
t.Errorf("版本号不对: %d", got.Version)
}
// 页码应该被排序过
for i := 1; i < len(got.DonePages); i++ {
if got.DonePages[i] < got.DonePages[i-1] {
t.Errorf("donePages 没排序: %v", got.DonePages)
break
}
}
if len(got.DonePages) != 3 {
t.Errorf("donePages 数量不对: %v", got.DonePages)
}
}
func TestManifestNotExist(t *testing.T) {
dir := t.TempDir()
m, err := ReadManifest(dir)
if err != nil {
t.Errorf("不存在时不该报错: %v", err)
}
if m != nil {
t.Error("不存在时该返回 nil")
}
}
func TestManifestCorrupted(t *testing.T) {
dir := t.TempDir()
os.WriteFile(filepath.Join(dir, ManifestFileName), []byte("{坏掉的 json"), 0o644)
if _, err := ReadManifest(dir); err == nil {
t.Error("坏 json 应该报错")
}
}
func TestScanManifests(t *testing.T) {
root := t.TempDir()
// 两个正常画廊
d1 := filepath.Join(root, "画廊A")
d2 := filepath.Join(root, "画廊B")
os.MkdirAll(d1, 0o755)
os.MkdirAll(d2, 0o755)
WriteManifest(d1, Manifest{GID: "111", Token: "a", TotalPages: 10, DonePages: []int{1, 2}})
WriteManifest(d2, Manifest{GID: "222", Token: "b", TotalPages: 5, DonePages: []int{1}})
// 一个非画廊目录(没清单)
os.MkdirAll(filepath.Join(root, "普通文件夹"), 0o755)
// 一个隐藏目录(该跳过)
hidden := filepath.Join(root, ".hidden")
os.MkdirAll(hidden, 0o755)
WriteManifest(hidden, Manifest{GID: "999", Token: "z"})
got := ScanManifests(root)
if len(got) != 2 {
t.Fatalf("应扫到 2 个清单,实际 %d: %+v", len(got), got)
}
found := map[string]bool{}
for _, m := range got {
found[m.GID] = true
}
if !found["111"] || !found["222"] {
t.Errorf("扫到的画廊不对: %v", found)
}
if found["999"] {
t.Error("隐藏目录不该被扫到")
}
}
// 用清单补全丢失的任务信息
func TestReconcileTask(t *testing.T) {
m := &Manifest{
GID: "111",
TotalPages: 206,
Title: "从清单恢复的标题",
DonePages: []int{1, 2, 3, 7},
}
// 模拟 state.json 丢了的任务
task := &Task{GID: "111", Token: "a", Pages: map[string]string{}}
if !ReconcileTask(task, m) {
t.Error("应该有修改")
}
if task.Title != "从清单恢复的标题" {
t.Errorf("标题没补上: %q", task.Title)
}
if task.TotalPages != 206 {
t.Errorf("总页数没补上: %d", task.TotalPages)
}
if len(task.Pages) != 4 {
t.Errorf("已完成页没补上: %v", task.Pages)
}
if task.Pages["7"] != "done" {
t.Errorf("第 7 页该是 done: %v", task.Pages)
}
// 已有信息不该被覆盖
task2 := &Task{
GID: "111", Title: "已有标题", TotalPages: 10,
Pages: map[string]string{"1": "done", "2": "done", "3": "done", "7": "done"},
}
if ReconcileTask(task2, m) {
t.Error("清单内容都已存在时不该报改动")
}
if task2.Title != "已有标题" {
t.Errorf("已有标题被覆盖了: %q", task2.Title)
}
if task2.TotalPages != 10 {
t.Errorf("已有总页数被覆盖了: %d", task2.TotalPages)
}
// 完全空白任务 + 空清单:不该有任何改动
empty := &Task{GID: "999", Pages: map[string]string{}}
if ReconcileTask(empty, &Manifest{GID: "999"}) {
t.Error("空清单不该产生改动")
}
}
// state.json 丢失的真实场景:完整走一遍
func TestManifestEnablesResumeWithoutState(t *testing.T) {
root := t.TempDir()
dir := filepath.Join(root, "某画廊")
os.MkdirAll(dir, 0o755)
// 模拟:之前下过 3 张,清单记录了
WriteManifest(dir, Manifest{
GID: "4239274",
Token: "abc",
Title: "测试",
TotalPages: 10,
DonePages: []int{1, 2, 3},
})
for i := 1; i <= 3; i++ {
os.WriteFile(filepath.Join(dir, "00"+string(rune('0'+i))+".png"), []byte("data"), 0o644)
}
// 现在 state.json 完全丢失,只能靠清单 + 文件系统
ms := ScanManifests(root)
if len(ms) != 1 {
t.Fatalf("应扫到 1 个,实际 %d", len(ms))
}
m := ms[0]
if m.TotalPages != 10 {
t.Errorf("总页数不该: %d", m.TotalPages)
}
// 靠文件系统知道哪几张有了
have := 0
for i := 1; i <= m.TotalPages; i++ {
name := "00" + string(rune('0'+i)) + ".png"
if st, err := os.Stat(filepath.Join(dir, name)); err == nil && st.Size() > 0 {
have++
}
}
if have != 3 {
t.Errorf("应有 3 张已下载,实际 %d", have)
}
// 还差 7 张 —— 这是清单带来的价值
if m.TotalPages-have != 7 {
t.Errorf("应还差 7 张,实际 %d", m.TotalPages-have)
}
}
func TestFindTaskByGID(t *testing.T) {
s, _ := newTestStore(t)
s.AddTasks([]eh.GalleryRef{{GID: "111", Token: "tokA"}})
// token 对不上也能按 gid 找到
task := s.FindTaskByGID("111")
if task == nil {
t.Fatal("应该找得到")
}
if task.Token != "tokA" {
t.Errorf("token 不对: %s", task.Token)
}
if s.FindTaskByGID("999") != nil {
t.Error("不存在的 gid 应返回 nil")
}
// token 精确匹配仍要正常工作
if s.FindTask("111", "tokA") == nil {
t.Error("精确查找应该能找到")
}
}