117 lines
4.2 KiB
Go
117 lines
4.2 KiB
Go
package collector
|
||
|
||
// foldertree_live_test.go — 對**真實的監看資料夾**跑一次樹,用 leo 給的機械判準對帳
|
||
//(`inkstone/Arcrun#180`,leo 2026-08-28:
|
||
// 「就在桌面,你為什麼沒看?最簡單的方式就像 terminal tree 一樣把被同步資料夾下的
|
||
// 所有 folder 都遍歷後印出來」)。
|
||
//
|
||
// 🔴 為什麼 fixture 不夠(同 sourcerepair_live_test.go 的理由):
|
||
// `foldertree_traversal_test.go` 用的是我們自己擺出來的形狀——它證明得了邏輯,
|
||
// 證明不了「**leo 桌面上那個資料夾**現在到底展不展得開」。
|
||
// 這張票的現象是他螢幕上的數字,對帳就要拿他螢幕上那個資料夾來對。
|
||
//
|
||
// 只讀磁碟、不碰網路、不寫 manifest,所以沒有「打錯實例」的風險;
|
||
// 仍然預設 skip,因為路徑是機器專屬的。
|
||
//
|
||
// LIVE_TREE_ROOTS="/Users/x/Desktop/youlinhsieh-test1,/Users/x/Desktop/youlinhsieh-test2" \
|
||
// go test -run TestLiveFolderTreeMatchesDisk -v ./
|
||
//
|
||
// 判準(leo 原話):**`find <root> -type d` 數出幾個目錄,雲端的樹就該有幾個節點**
|
||
//(收不收是另一回事,`skip_reason` 照標)。
|
||
// 例外只有一種且必須說得出口:**真的沒走進去**的那幾類(gitignore/工具產物/
|
||
// 建置目錄/worktree/巢狀 repo)——它們仍然佔一個節點,但底下不生子節點,
|
||
// 所以本測試把它們的子樹從地端這一側也扣掉,兩邊才是同一個問題的兩個答案。
|
||
|
||
import (
|
||
"io/fs"
|
||
"os"
|
||
"path/filepath"
|
||
"sort"
|
||
"strings"
|
||
"testing"
|
||
"time"
|
||
)
|
||
|
||
// diskDirsExcludingPrunedSubtrees=地端目錄清單,但「真的沒走進去」的那幾類只算它自己。
|
||
func diskDirsExcludingPrunedSubtrees(t *testing.T, root string, plan IngestPlan) []string {
|
||
t.Helper()
|
||
var out []string
|
||
err := filepath.WalkDir(root, func(p string, d fs.DirEntry, err error) error {
|
||
if err != nil || !d.IsDir() {
|
||
return err //nolint:nilerr // 讀不到的目錄交給 WalkDir 自己回報
|
||
}
|
||
if p == root {
|
||
out = append(out, "")
|
||
return nil
|
||
}
|
||
if strings.HasPrefix(d.Name(), ".") {
|
||
return filepath.SkipDir
|
||
}
|
||
rel, _ := filepath.Rel(root, p)
|
||
rel = filepath.ToSlash(rel)
|
||
out = append(out, rel)
|
||
if skip, _ := plan.SkipsDirWhy(rel, p); skip {
|
||
return filepath.SkipDir // 節點留著(上一行已經 append),子樹不算
|
||
}
|
||
return nil
|
||
})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
sort.Strings(out)
|
||
return out
|
||
}
|
||
|
||
func TestLiveFolderTreeMatchesDisk(t *testing.T) {
|
||
raw := strings.TrimSpace(os.Getenv("LIVE_TREE_ROOTS"))
|
||
if raw == "" {
|
||
t.Skip("設 LIVE_TREE_ROOTS=<資料夾,資料夾> 才跑(路徑是機器專屬的)")
|
||
}
|
||
for _, root := range strings.Split(raw, ",") {
|
||
root = strings.TrimSpace(root)
|
||
if root == "" {
|
||
continue
|
||
}
|
||
t.Run(filepath.Base(root), func(t *testing.T) {
|
||
plan := PlanIngest(root)
|
||
m := &Manifest{Entries: map[string]*ManifestEntry{}}
|
||
p, err := Scan(root, m, ScanOptions{Plan: plan})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
tree := BuildFolderTree(root, "live", p.DirStats, m.Entries, p.AllExcludedDirs, plan, time.Now())
|
||
|
||
onTree := map[string]FolderNode{}
|
||
for _, n := range tree.Nodes {
|
||
onTree[n.Path] = n
|
||
}
|
||
want := diskDirsExcludingPrunedSubtrees(t, root, plan)
|
||
|
||
t.Logf("收檔策略:%s — %s", plan.Mode, plan.Reason)
|
||
t.Logf("地端目錄 %d(扣掉沒走進去的子樹)/樹上節點 %d/truncated=%v",
|
||
len(want), tree.TotalNodes, tree.Truncated)
|
||
for _, n := range tree.Nodes {
|
||
t.Logf(" %-46s tot=%-4d sync=%-4d excl=%-4d skipped=%v %s",
|
||
"'"+n.Path+"'", n.TotalFiles, n.SyncedFiles, n.ExcludedFiles, n.Skipped, n.SkipReason)
|
||
}
|
||
|
||
var missing []string
|
||
for _, w := range want {
|
||
if _, ok := onTree[w]; !ok {
|
||
missing = append(missing, w)
|
||
}
|
||
}
|
||
if len(missing) > 0 {
|
||
t.Fatalf("地端有、樹上沒有的資料夾(「不收」又被實作成「不走進去」了):%v", missing)
|
||
}
|
||
if tree.TotalNodes != len(want) {
|
||
t.Fatalf("樹上節點 %d ≠ 地端資料夾 %d", tree.TotalNodes, len(want))
|
||
}
|
||
if tree.Truncated {
|
||
t.Fatalf("被截斷了——MaxFolderTreeNodes=%d 不夠這個資料夾用,不准靜默截斷",
|
||
MaxFolderTreeNodes)
|
||
}
|
||
})
|
||
}
|
||
}
|