package web import ( "bytes" "os" "path/filepath" "strings" "testing" ) // mkFile 造一个指定字节数的候选文件。 func mkFile(t *testing.T, dir, name string, n int) string { t.Helper() p := filepath.Join(dir, name) if err := os.WriteFile(p, bytes.Repeat([]byte("x"), n), 0o644); err != nil { t.Fatalf("造 %s 失败: %v", name, err) } return p } // P5 回归:按摘要找回备份包时,必须先按字节数筛一遍再算摘要。 // // 只看「最终返回哪个文件」新旧写法完全一样——真正的差别是**算了几次 sha256**, // 而 uploads 里每份备份动辄几百 MB,多算一次就是几百 MB 的读盘。所以这里注入一个 // 会计数的 hash,把「有没有白算」直接钉住。 func TestPickArchiveSkipsWrongSizedFilesWithoutHashing(t *testing.T) { dir := t.TempDir() // 两个诱饵都比正主新(会排在候选前面),大小都和正主不同 d1 := mkFile(t, dir, "decoy-newest.rondbackup", 10) d2 := mkFile(t, dir, "decoy-mid.rondbackup", 20) right := mkFile(t, dir, "right.rondbackup", 30) hashed := 0 got := pickArchiveBySha([]string{d1, d2, right}, "sha-right", 30, func(f string) (string, error) { hashed++ // 按文件名给摘要:这样「选中谁」和「算了几次」互相独立—— // 去掉预筛时选中的仍然是 right,但白读了两个诱饵(hashed=3)。 return "sha-" + strings.TrimSuffix(filepath.Base(f), ".rondbackup"), nil }) if got != right { t.Errorf("应当选中 %s,实际 %s", filepath.Base(right), filepath.Base(got)) } if hashed != 1 { t.Errorf("只该对大小相符的那份算摘要,实际算了 %d 次(白读了 %d 份整包)", hashed, hashed-1) } } // 反向约束:筛选只许按大小筛,不许把大小恰好相符的候选也一起跳过。 // 三份大小相同、只有最后一份摘要对得上 ⇒ 必须老老实实算三次。 func TestPickArchiveHashesEverySameSizedCandidate(t *testing.T) { dir := t.TempDir() a := mkFile(t, dir, "a.rondbackup", 30) b := mkFile(t, dir, "b.rondbackup", 30) c := mkFile(t, dir, "c.rondbackup", 30) hashed := 0 got := pickArchiveBySha([]string{a, b, c}, "sha-c", 30, func(f string) (string, error) { hashed++ return "sha-" + strings.TrimSuffix(filepath.Base(f), ".rondbackup"), nil }) if got != c { t.Errorf("应当选中 c.rondbackup,实际 %s", filepath.Base(got)) } if hashed != 3 { t.Errorf("大小都相符时不能跳过任何一份,实际只算了 %d 次", hashed) } } // 退化路径:库里没记 sha(老数据)时不按大小筛,保持「取最新的那份」的旧行为—— // 否则会因为记不到 size 而突然一份都选不中,导出直接失败。 func TestPickArchiveKeepsNewestWhenNoSha(t *testing.T) { dir := t.TempDir() newest := mkFile(t, dir, "newest.rondbackup", 10) older := mkFile(t, dir, "older.rondbackup", 999) hashed := 0 got := pickArchiveBySha([]string{newest, older}, "", 0, func(f string) (string, error) { hashed++ return "whatever", nil }) if got != newest { t.Errorf("没有 sha 时应当取第一份可读的,实际 %s", filepath.Base(got)) } if hashed != 1 { t.Errorf("没有 sha 时不该多算,实际算了 %d 次", hashed) } } // 目录与读不到的文件要跳过,不能因为一份坏候选就整段失败。 func TestPickArchiveSkipsDirsAndUnreadable(t *testing.T) { dir := t.TempDir() if err := os.Mkdir(filepath.Join(dir, "subdir"), 0o755); err != nil { t.Fatal(err) } missing := filepath.Join(dir, "does-not-exist.rondbackup") ok := mkFile(t, dir, "ok.rondbackup", 5) got := pickArchiveBySha([]string{missing, filepath.Join(dir, "subdir"), ok}, "sha-ok", 5, func(f string) (string, error) { if f == ok { return "sha-ok", nil } t.Errorf("不该对 %s 算摘要", f) return "nope", nil }) if got != ok { t.Errorf("应当跳过缺失与目录后选中 ok.rondbackup,实际 %q", got) } // 找不到时返回空串(调用方据此报「请重新上传」) if s := pickArchiveBySha([]string{ok}, "sha-none", 5, func(string) (string, error) { return "other", nil }); s != "" { t.Errorf("没有命中时应返回空串,实际 %q", s) } } // findSourceArchive 必须把 size_bytes 一起查出来——只查 sha 的话上面那道筛选就永远不生效, // 而且编译器不会拦(筛挂掉、越筛越慢,行为测试看不出来)。 func TestFindSourceArchiveConsultsSizeBytes(t *testing.T) { src, err := os.ReadFile(pkgFile(t, "handlers_export.go")) if err != nil { t.Fatalf("读 handlers_export.go 失败: %v", err) } body := sliceFunc(t, string(src), "func (s *Server) findSourceArchive(") if !strings.Contains(body, "size_bytes") { t.Error("findSourceArchive 没有读取 size_bytes:按大小预筛会永远拿不到目标大小") } if !strings.Contains(body, "pickArchiveBySha(") { t.Error("findSourceArchive 没有走 pickArchiveBySha:按大小预筛没有生效") } }