From 2080b3d031b1c88134be1638e8b745fa6e941b1d Mon Sep 17 00:00:00 2001 From: Feng Ruohang Date: Sun, 18 Oct 2026 07:23:05 +0800 Subject: [PATCH] test(listing): cover mixed object and directory recovery groups Extend the pending-directory collision test with absent recovery and a smaller key. Add all six normal-object, directory, and pending-object channel orders, checking complete-group recovery and preservation of trailing entries. Test changes adapted from commit 557bc274a32330321fa5e5ac7636068c7bd98eb0. Retain the complete-group directory handling from 3dd53eb81f20. Signed-off-by: Feng Ruohang --- cmd/metacache-listing-directory_test.go | 62 +++++++++++++++++++++++ cmd/metacache-listing-recovery_test.go | 67 ++++++++++++++++--------- 2 files changed, 104 insertions(+), 25 deletions(-) diff --git a/cmd/metacache-listing-directory_test.go b/cmd/metacache-listing-directory_test.go index 6718f08f2..70d61b524 100644 --- a/cmd/metacache-listing-directory_test.go +++ b/cmd/metacache-listing-directory_test.go @@ -179,3 +179,65 @@ func TestListingRecoveryDirectoryCollisionOrdering(t *testing.T) { } } } + +// A regular walk result may be stale when a pending peer forces current +// recovery. Keep sibling prefixes until the complete object group is resolved. +func TestListingRecoveryMixedDirectoryCollision(t *testing.T) { + orders := [][3]int{{0, 1, 2}, {0, 2, 1}, {1, 0, 2}, {1, 2, 0}, {2, 0, 1}, {2, 1, 0}} + for _, order := range orders { + for _, present := range []bool{false, true} { + t.Run(fmt.Sprintf("order=%v/present=%t", order, present), func(t *testing.T) { + entries := []metaCacheEntry{ + {name: "dir/", metadata: []byte{1}}, + {name: "dir/"}, + {name: "dir/", listingSet: &erasureObjects{}}, + } + in := make([]chan metaCacheEntry, len(order)) + for i, index := range order { + in[i] = make(chan metaCacheEntry, 2) + in[i] <- entries[index] + in[i] <- metaCacheEntry{name: fmt.Sprintf("tail-%d", i), metadata: []byte{1}} + close(in[i]) + } + out := make(chan metaCacheEntry, 6) + calls := 0 + err := mergeEntryChannelsWithResolver(t.Context(), in, out, 1, nil, func(group []*metaCacheEntry) (*metaCacheEntry, error) { + calls++ + if len(group) != 2 { + t.Fatalf("incomplete object group: %+v", group) + } + pending := 0 + for _, entry := range group { + if entry.name != "dir/" || entry.isDir() { + t.Fatalf("unexpected recovery entry: %+v", entry) + } + if entry.listingSet != nil { + pending++ + } + } + if pending != 1 { + t.Fatalf("pending entries=%d, want 1", pending) + } + if !present { + return nil, nil + } + return &metaCacheEntry{name: "dir/", metadata: []byte{1}}, nil + }) + if err != nil || calls != 1 { + t.Fatalf("err=%v recovery calls=%d", err, calls) + } + var names []string + for entry := range out { + names = append(names, entry.name) + if entry.listingSet != nil || entry.name == "dir/" && entry.isObject() != present { + t.Fatalf("unexpected merged entry: %+v", entry) + } + } + want := []string{"dir/", "tail-0", "tail-1", "tail-2"} + if !slices.Equal(names, want) { + t.Fatalf("names=%v, want %v", names, want) + } + }) + } + } +} diff --git a/cmd/metacache-listing-recovery_test.go b/cmd/metacache-listing-recovery_test.go index 57daef0bf..abe67f2f7 100644 --- a/cmd/metacache-listing-recovery_test.go +++ b/cmd/metacache-listing-recovery_test.go @@ -221,33 +221,50 @@ func TestListingRecoveryNormalPathDoesNotReadOrLock(t *testing.T) { func TestListingPendingDirectoryCollision(t *testing.T) { for _, objectFirst := range []bool{false, true} { - t.Run(fmt.Sprint(objectFirst), func(t *testing.T) { - in := []chan metaCacheEntry{make(chan metaCacheEntry, 1), make(chan metaCacheEntry, 1)} - objectIndex := 1 - if objectFirst { - objectIndex = 0 - } - in[objectIndex] <- metaCacheEntry{name: "dir/", listingSet: &erasureObjects{}} - in[1-objectIndex] <- metaCacheEntry{name: "dir/"} - for _, c := range in { - close(c) - } - out := make(chan metaCacheEntry, 2) - calls := 0 - err := mergeEntryChannelsWithResolver(t.Context(), in, out, 1, nil, func(group []*metaCacheEntry) (*metaCacheEntry, error) { - calls++ - if len(group) != 1 || group[0].listingSet == nil { - t.Fatalf("wrong pending group: %+v", group) + for _, present := range []bool{true, false} { + t.Run(fmt.Sprintf("objectFirst=%v/present=%v", objectFirst, present), func(t *testing.T) { + in := []chan metaCacheEntry{make(chan metaCacheEntry, 2), make(chan metaCacheEntry, 2), make(chan metaCacheEntry, 2)} + objectIndex := 1 + if objectFirst { + objectIndex = 0 + } + in[objectIndex] <- metaCacheEntry{name: "dir/", listingSet: &erasureObjects{}} + in[1-objectIndex] <- metaCacheEntry{name: "dir/"} + in[2] <- metaCacheEntry{name: "a", metadata: []byte{1}} + for _, c := range in { + close(c) + } + out := make(chan metaCacheEntry, 4) + calls := 0 + err := mergeEntryChannelsWithResolver(t.Context(), in, out, 1, nil, func(group []*metaCacheEntry) (*metaCacheEntry, error) { + calls++ + if len(group) != 1 || group[0].listingSet == nil { + t.Fatalf("wrong pending group: %+v", group) + } + if !present { + return nil, nil + } + return &metaCacheEntry{name: "dir/", metadata: []byte{1}}, nil + }) + if err != nil || calls != 1 || len(out) != 2 { + t.Fatalf("err=%v recoveries=%d outputs=%d", err, calls, len(out)) + } + first := <-out + if first.name != "a" { + t.Fatalf("expected smaller key first, got %+v", first) + } + entry := <-out + if entry.name != "dir/" || entry.listingSet != nil { + t.Fatalf("unexpected recovered entry: %+v", entry) + } + if present && (entry.isDir() || !entry.isObjectDir()) { + t.Fatalf("object became directory: %+v", entry) + } + if !present && (!entry.isDir() || entry.name != "dir/") { + t.Fatalf("absent pending object dropped live directory prefix: %+v", entry) } - return &metaCacheEntry{name: "dir/", metadata: []byte{1}}, nil }) - if err != nil || calls != 1 || len(out) != 1 { - t.Fatalf("err=%v recoveries=%d outputs=%d", err, calls, len(out)) - } - if entry := <-out; entry.isDir() || !entry.isObjectDir() { - t.Fatalf("object became directory: %+v", entry) - } - }) + } } }