snapshot of https://github.com/seaweedfs/seaweedfs.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
go: build dev binaries / cleanup (push) Successful in 4s
Codespell / Check for spelling errors (push) Failing after 3s
docker: build dev containers / build-rust-binaries (amd64, x86_64-unknown-linux-musl) (push) Failing after 3s
docker: build dev containers / build-dev-containers (push) Has been skipped
docker: build dev containers / build-rust-binaries (arm64, true, aarch64-unknown-linux-musl) (push) Failing after 3s
End to End / FUSE Mount (push) Failing after 2s
EC Integration Tests / EC Integration Tests (push) Failing after 30s
FUSE DLM Integration Tests / FUSE DLM Integration Tests (push) Failing after 3s
FUSE Volume Server Failover Tests / FUSE Volume Server Failover (push) Failing after 3s
FUSE Integration Tests / FUSE Integration Testing (push) Failing after 2s
FUSE P2P Peer Chunk Sharing Integration Tests / FUSE P2P Peer Chunk Sharing (push) Failing after 3s
go: build binary / Go Vet (push) Failing after 2s
go: build binary / Go Vet 32-bit (push) Failing after 3s
go: build binary / Build (push) Failing after 4s
go: build binary / Build cross-platform (freebsd/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (darwin/arm64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/arm64) (push) Failing after 3s
go: build binary / Test (push) Failing after 2s
go: build binary / Test 32-bit (push) Failing after 3s
helm: lint and test charts / lint-test (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (11) (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (17) (push) Failing after 3s
go: build dev binaries / build_dev_linux_windows (amd64, linux) (push) Failing after 2s
go: build dev binaries / build_dev_linux_windows (amd64, windows) (push) Failing after 3s
go: build dev binaries / build_dev_darwin (amd64, darwin) (push) Has been skipped
go: build dev binaries / build_dev_darwin (arm64, darwin) (push) Has been skipped
Java Client Unit Tests / Java Unit Tests (11) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (17) (push) Failing after 2s
Java Client Unit Tests / Java Unit Tests (21) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (8) (push) Failing after 3s
Kafka Quick Test (Load Test with Schema Registry) / Kafka Client Load Test (Quick) (push) Failing after 3s
Kafka Gateway Tests / Kafka Unit Tests (unit-tests-1) (push) Failing after 11s
Kafka Gateway Tests / Kafka Integration Tests (Critical) (integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka End-to-End Tests (with SMQ) (e2e-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Consumer Group Tests (Highly Isolated) (consumer-group-1) (push) Failing after 2s
Kafka Gateway Tests / Kafka Client Compatibility (with SMQ) (client-compat-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka SMQ Integration Tests (Full Stack) (smq-integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Protocol Tests (Isolated) (protocol-1) (push) Failing after 3s
KMS Tests / KMS Provider Integration Tests (push) Failing after 2s
KMS Tests / S3 KMS End-to-End Tests (push) Failing after 3s
Master Cold Start Tests / Master Cold Start Tests (push) Failing after 4s
Metadata Subscribe Integration Tests / Metadata Subscribe Integration Tests (push) Failing after 3s
Multi-Master Tests / Multi-Master Failover Tests (push) Failing after 3s
Performance / CPU and Heap Profile (push) Failing after 2s
Performance / S3 Read/Write Benchmark (rust volume) (push) Failing after 3s
pjdfstest POSIX Compliance / pjdfstest (push) Failing after 2s
Plugin Worker Integration Tests / Plugin Worker: erasure_coding (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: vacuum (push) Failing after 3s
Rust Volume Server Tests / Rust Integration Tests (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 1) (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 1) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 3) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Distributed Tests (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Performance Tests (push) Failing after 3s
Performance / Throughput Benchmark (go volume) (push) Failing after 3s
Performance / Throughput Benchmark (rust volume) (push) Failing after 2s
Performance / S3 Read/Write Benchmark (go volume) (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: volume_balance (push) Failing after 3s
PostgreSQL Gateway Tests / PostgreSQL Basic Tests (push) Failing after 3s
Rust Volume Server Tests / Rust Unit Tests (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 3) (push) Failing after 3s
Rust Plugin Worker Tests / Rust Plugin Worker Build and Unit Tests (push) Failing after 3s
rust: build dev volume server binaries / cleanup (push) Successful in 3s
S3 ETag and ACL Tests / S3 ETag + ACL Integration Tests (push) Failing after 3s
S3 IAM Integration Tests / IAM Unit Tests (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (advanced) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (basic) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (group) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (policy-enforcement) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (sts) (push) Failing after 4s
S3 Keycloak Integration Tests / S3 Keycloak Integration Tests (push) Failing after 3s
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.11) (push) Failing after 3s
S3 PyArrow Parquet Tests / Go Unit Tests (Implicit Directory) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.12) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-amd64, x86_64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-arm64, aarch64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-linux (linux-amd64, x86_64-unknown-linux-gnu) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.9) (push) Has been cancelled
S3 Proxy Signature Tests / S3 Proxy Signature Verification Tests (push) Has been cancelled
S3 SDK V2 Route Disambiguation Tests / S3 SDK V2 Routing Tests (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (quick) (push) Has been cancelled
S3 SSE Tests / S3 SSE Compatibility Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Metadata Persistence Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Copy Operations Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Multipart Upload Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Performance Test (push) Has been cancelled
S3 SSE Tests / S3 Volume Encryption Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (comprehensive) (push) Has been cancelled
Spark Integration Tests / Spark Integration Tests (push) Failing after 2s
Telemetry Integration Tests / telemetry-integration-test (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 1) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 1) (push) Failing after 3s
Ceph S3 tests / Basic S3 tests (KV store) (push) Has been cancelled
Ceph S3 tests / S3 Versioning & Object Lock tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Unit Tests (push) Failing after 3s
Ceph S3 tests / S3 CORS tests (push) Has been cancelled
Ceph S3 tests / SeaweedFS Custom S3 Copy tests (push) Has been cancelled
Ceph S3 tests / Basic S3 tests (SQL store) (push) Has been cancelled
test s3 over https using aws-cli / awscli-tests (push) Has been cancelled
TLS Rotation Integration Tests / TLS Rotation Integration Tests (push) Has been cancelled
Vacuum Integration Tests / Vacuum Integration Tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Variables Integration Tests (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (advanced-policy) (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (basic-policy) (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (contextual-policy) (push) Failing after 4s
S3 Policy Integration Tests / Trusted Proxy Detection Tests (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Shell Integration Tests (push) Failing after 3s
Samba on FUSE Integration / samba-integration (push) Failing after 2s
SFTP Integration Tests / SFTP Integration Testing (push) Failing after 3s
terraform: validate and test modules / fmt, validate, plan-level tests (push) Failing after 3s
terraform: validate and test modules / local cluster smoke test (real weed) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 3) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 2) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 2) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 3) (push) Failing after 5s
mount: windows conformance / WinFsp conformance (push) Has been cancelled
mount: windows / Mount on Windows (push) Has been cancelled
Some checks failed
go: build dev binaries / cleanup (push) Successful in 4s
Codespell / Check for spelling errors (push) Failing after 3s
docker: build dev containers / build-rust-binaries (amd64, x86_64-unknown-linux-musl) (push) Failing after 3s
docker: build dev containers / build-dev-containers (push) Has been skipped
docker: build dev containers / build-rust-binaries (arm64, true, aarch64-unknown-linux-musl) (push) Failing after 3s
End to End / FUSE Mount (push) Failing after 2s
EC Integration Tests / EC Integration Tests (push) Failing after 30s
FUSE DLM Integration Tests / FUSE DLM Integration Tests (push) Failing after 3s
FUSE Volume Server Failover Tests / FUSE Volume Server Failover (push) Failing after 3s
FUSE Integration Tests / FUSE Integration Testing (push) Failing after 2s
FUSE P2P Peer Chunk Sharing Integration Tests / FUSE P2P Peer Chunk Sharing (push) Failing after 3s
go: build binary / Go Vet (push) Failing after 2s
go: build binary / Go Vet 32-bit (push) Failing after 3s
go: build binary / Build (push) Failing after 4s
go: build binary / Build cross-platform (freebsd/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (darwin/arm64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/arm64) (push) Failing after 3s
go: build binary / Test (push) Failing after 2s
go: build binary / Test 32-bit (push) Failing after 3s
helm: lint and test charts / lint-test (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (11) (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (17) (push) Failing after 3s
go: build dev binaries / build_dev_linux_windows (amd64, linux) (push) Failing after 2s
go: build dev binaries / build_dev_linux_windows (amd64, windows) (push) Failing after 3s
go: build dev binaries / build_dev_darwin (amd64, darwin) (push) Has been skipped
go: build dev binaries / build_dev_darwin (arm64, darwin) (push) Has been skipped
Java Client Unit Tests / Java Unit Tests (11) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (17) (push) Failing after 2s
Java Client Unit Tests / Java Unit Tests (21) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (8) (push) Failing after 3s
Kafka Quick Test (Load Test with Schema Registry) / Kafka Client Load Test (Quick) (push) Failing after 3s
Kafka Gateway Tests / Kafka Unit Tests (unit-tests-1) (push) Failing after 11s
Kafka Gateway Tests / Kafka Integration Tests (Critical) (integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka End-to-End Tests (with SMQ) (e2e-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Consumer Group Tests (Highly Isolated) (consumer-group-1) (push) Failing after 2s
Kafka Gateway Tests / Kafka Client Compatibility (with SMQ) (client-compat-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka SMQ Integration Tests (Full Stack) (smq-integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Protocol Tests (Isolated) (protocol-1) (push) Failing after 3s
KMS Tests / KMS Provider Integration Tests (push) Failing after 2s
KMS Tests / S3 KMS End-to-End Tests (push) Failing after 3s
Master Cold Start Tests / Master Cold Start Tests (push) Failing after 4s
Metadata Subscribe Integration Tests / Metadata Subscribe Integration Tests (push) Failing after 3s
Multi-Master Tests / Multi-Master Failover Tests (push) Failing after 3s
Performance / CPU and Heap Profile (push) Failing after 2s
Performance / S3 Read/Write Benchmark (rust volume) (push) Failing after 3s
pjdfstest POSIX Compliance / pjdfstest (push) Failing after 2s
Plugin Worker Integration Tests / Plugin Worker: erasure_coding (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: vacuum (push) Failing after 3s
Rust Volume Server Tests / Rust Integration Tests (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 1) (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 1) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 3) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Distributed Tests (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Performance Tests (push) Failing after 3s
Performance / Throughput Benchmark (go volume) (push) Failing after 3s
Performance / Throughput Benchmark (rust volume) (push) Failing after 2s
Performance / S3 Read/Write Benchmark (go volume) (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: volume_balance (push) Failing after 3s
PostgreSQL Gateway Tests / PostgreSQL Basic Tests (push) Failing after 3s
Rust Volume Server Tests / Rust Unit Tests (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 3) (push) Failing after 3s
Rust Plugin Worker Tests / Rust Plugin Worker Build and Unit Tests (push) Failing after 3s
rust: build dev volume server binaries / cleanup (push) Successful in 3s
S3 ETag and ACL Tests / S3 ETag + ACL Integration Tests (push) Failing after 3s
S3 IAM Integration Tests / IAM Unit Tests (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (advanced) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (basic) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (group) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (policy-enforcement) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (sts) (push) Failing after 4s
S3 Keycloak Integration Tests / S3 Keycloak Integration Tests (push) Failing after 3s
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.11) (push) Failing after 3s
S3 PyArrow Parquet Tests / Go Unit Tests (Implicit Directory) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.12) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-amd64, x86_64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-arm64, aarch64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-linux (linux-amd64, x86_64-unknown-linux-gnu) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.9) (push) Has been cancelled
S3 Proxy Signature Tests / S3 Proxy Signature Verification Tests (push) Has been cancelled
S3 SDK V2 Route Disambiguation Tests / S3 SDK V2 Routing Tests (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (quick) (push) Has been cancelled
S3 SSE Tests / S3 SSE Compatibility Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Metadata Persistence Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Copy Operations Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Multipart Upload Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Performance Test (push) Has been cancelled
S3 SSE Tests / S3 Volume Encryption Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (comprehensive) (push) Has been cancelled
Spark Integration Tests / Spark Integration Tests (push) Failing after 2s
Telemetry Integration Tests / telemetry-integration-test (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 1) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 1) (push) Failing after 3s
Ceph S3 tests / Basic S3 tests (KV store) (push) Has been cancelled
Ceph S3 tests / S3 Versioning & Object Lock tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Unit Tests (push) Failing after 3s
Ceph S3 tests / S3 CORS tests (push) Has been cancelled
Ceph S3 tests / SeaweedFS Custom S3 Copy tests (push) Has been cancelled
Ceph S3 tests / Basic S3 tests (SQL store) (push) Has been cancelled
test s3 over https using aws-cli / awscli-tests (push) Has been cancelled
TLS Rotation Integration Tests / TLS Rotation Integration Tests (push) Has been cancelled
Vacuum Integration Tests / Vacuum Integration Tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Variables Integration Tests (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (advanced-policy) (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (basic-policy) (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (contextual-policy) (push) Failing after 4s
S3 Policy Integration Tests / Trusted Proxy Detection Tests (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Shell Integration Tests (push) Failing after 3s
Samba on FUSE Integration / samba-integration (push) Failing after 2s
SFTP Integration Tests / SFTP Integration Testing (push) Failing after 3s
terraform: validate and test modules / fmt, validate, plan-level tests (push) Failing after 3s
terraform: validate and test modules / local cluster smoke test (real weed) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 3) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 2) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 2) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 3) (push) Failing after 5s
mount: windows conformance / WinFsp conformance (push) Has been cancelled
mount: windows / Mount on Windows (push) Has been cancelled
This commit is contained in:
969
weed/command/filer_sync_verify_test.go
Normal file
969
weed/command/filer_sync_verify_test.go
Normal file
@@ -0,0 +1,969 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
||||
"github.com/seaweedfs/seaweedfs/weed/util"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/metadata"
|
||||
)
|
||||
|
||||
// --- stream / inner-client / outer-client mocks ---
|
||||
|
||||
type verifyTestStream struct {
|
||||
entries []*filer_pb.Entry
|
||||
idx int
|
||||
recvErr error // returned after all entries instead of io.EOF, if set
|
||||
}
|
||||
|
||||
func (s *verifyTestStream) Recv() (*filer_pb.ListEntriesResponse, error) {
|
||||
if s.idx >= len(s.entries) {
|
||||
if s.recvErr != nil {
|
||||
return nil, s.recvErr
|
||||
}
|
||||
return nil, io.EOF
|
||||
}
|
||||
resp := &filer_pb.ListEntriesResponse{Entry: s.entries[s.idx]}
|
||||
s.idx++
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (s *verifyTestStream) Header() (metadata.MD, error) { return metadata.MD{}, nil }
|
||||
func (s *verifyTestStream) Trailer() metadata.MD { return metadata.MD{} }
|
||||
func (s *verifyTestStream) CloseSend() error { return nil }
|
||||
func (s *verifyTestStream) Context() context.Context { return context.Background() }
|
||||
func (s *verifyTestStream) SendMsg(_ any) error { return nil }
|
||||
func (s *verifyTestStream) RecvMsg(_ any) error { return nil }
|
||||
|
||||
// verifyTestInnerClient is the SeaweedFilerClient passed to fn inside WithFilerClient.
|
||||
type verifyTestInnerClient struct {
|
||||
filer_pb.SeaweedFilerClient // embed for unimplemented RPCs
|
||||
entriesByDir map[string][]*filer_pb.Entry
|
||||
recvErr error // injected listing error, if set
|
||||
}
|
||||
|
||||
func (c *verifyTestInnerClient) ListEntries(_ context.Context, in *filer_pb.ListEntriesRequest, _ ...grpc.CallOption) (grpc.ServerStreamingClient[filer_pb.ListEntriesResponse], error) {
|
||||
return &verifyTestStream{entries: c.entriesByDir[in.Directory], recvErr: c.recvErr}, nil
|
||||
}
|
||||
|
||||
// verifyTestFilerClient implements filer_pb.FilerClient and tracks concurrent
|
||||
// WithFilerClient invocations to let tests verify the global concurrency bound.
|
||||
// inFlight/peakFlight use atomic.Int64 (not bare int64 + sync/atomic funcs) so
|
||||
// the fields are 8-byte aligned even on 32-bit platforms — bare int64 fields
|
||||
// after a pointer-sized field panic with "unaligned 64-bit atomic operation"
|
||||
// on 386/arm/mips.
|
||||
type verifyTestFilerClient struct {
|
||||
entriesByDir map[string][]*filer_pb.Entry
|
||||
inFlight atomic.Int64
|
||||
peakFlight atomic.Int64
|
||||
delay time.Duration
|
||||
onList func() // called at the start of each listing, if set
|
||||
recvErr error // injected listing error, surfaced after entries
|
||||
}
|
||||
|
||||
func (c *verifyTestFilerClient) WithFilerClient(_ bool, fn func(filer_pb.SeaweedFilerClient) error) error {
|
||||
// track peak concurrent in-flight listings
|
||||
n := c.inFlight.Add(1)
|
||||
defer c.inFlight.Add(-1)
|
||||
for {
|
||||
peak := c.peakFlight.Load()
|
||||
if n <= peak || c.peakFlight.CompareAndSwap(peak, n) {
|
||||
break
|
||||
}
|
||||
}
|
||||
if c.onList != nil {
|
||||
c.onList()
|
||||
}
|
||||
if c.delay > 0 {
|
||||
time.Sleep(c.delay)
|
||||
}
|
||||
return fn(&verifyTestInnerClient{entriesByDir: c.entriesByDir, recvErr: c.recvErr})
|
||||
}
|
||||
|
||||
func (c *verifyTestFilerClient) AdjustedUrl(_ *filer_pb.Location) string { return "" }
|
||||
func (c *verifyTestFilerClient) GetDataCenter() string { return "" }
|
||||
|
||||
// --- entry helpers ---
|
||||
|
||||
func verifyFileEntry(name string, size uint64) *filer_pb.Entry {
|
||||
return &filer_pb.Entry{
|
||||
Name: name,
|
||||
Attributes: &filer_pb.FuseAttributes{FileSize: size},
|
||||
}
|
||||
}
|
||||
|
||||
func verifyDirEntry(name string) *filer_pb.Entry {
|
||||
return &filer_pb.Entry{Name: name, IsDirectory: true}
|
||||
}
|
||||
|
||||
// verifyChunk builds a FileChunk whose ETag is the base64 MD5 of seed, so
|
||||
// filer.ETagChunks (which base64-decodes each chunk ETag) can hash it. Distinct
|
||||
// seeds yield distinct per-chunk MD5s.
|
||||
func verifyChunk(offset int64, size uint64, seed string) *filer_pb.FileChunk {
|
||||
return &filer_pb.FileChunk{
|
||||
Offset: offset,
|
||||
Size: size,
|
||||
ETag: util.Base64Encode(util.Md5([]byte(seed))),
|
||||
}
|
||||
}
|
||||
|
||||
// verifyChunkedEntry builds a chunk-backed entry with no stored attr.Md5, so
|
||||
// its file ETag is derived from ETagChunks (the order-sensitive path). FileSize
|
||||
// is the max chunk end, matching filer.FileSize, so size comparison passes.
|
||||
func verifyChunkedEntry(name string, chunks []*filer_pb.FileChunk) *filer_pb.Entry {
|
||||
var total uint64
|
||||
for _, c := range chunks {
|
||||
if end := uint64(c.Offset) + c.Size; end > total {
|
||||
total = end
|
||||
}
|
||||
}
|
||||
return &filer_pb.Entry{
|
||||
Name: name,
|
||||
Attributes: &filer_pb.FuseAttributes{FileSize: total}, // Md5 nil → ETagChunks path
|
||||
Chunks: chunks,
|
||||
}
|
||||
}
|
||||
|
||||
// --- tests ---
|
||||
|
||||
// TestVerifySyncMissingFile confirms that a file present in A but absent in B
|
||||
// is counted as missing.
|
||||
func TestVerifySyncMissingFile(t *testing.T) {
|
||||
clientA := &verifyTestFilerClient{
|
||||
entriesByDir: map[string][]*filer_pb.Entry{
|
||||
"/root": {verifyFileEntry("file.txt", 100)},
|
||||
},
|
||||
}
|
||||
clientB := &verifyTestFilerClient{
|
||||
entriesByDir: map[string][]*filer_pb.Entry{
|
||||
"/root": {},
|
||||
},
|
||||
}
|
||||
|
||||
result := &VerifyResult{}
|
||||
sem := make(chan struct{}, verifySyncConcurrency)
|
||||
err := compareDirectory(context.Background(), clientA, clientB,
|
||||
"/root", "/root", false, time.Time{}, sem, result)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if got := result.missingCount.Load(); got != 1 {
|
||||
t.Errorf("missingCount = %d, want 1", got)
|
||||
}
|
||||
if got := result.sizeMismatch.Load(); got != 0 {
|
||||
t.Errorf("sizeMismatch = %d, want 0", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestVerifySyncOnlyInB confirms that a file present only in B is counted
|
||||
// (non-active-passive mode) or ignored (active-passive mode).
|
||||
func TestVerifySyncOnlyInB(t *testing.T) {
|
||||
clientA := &verifyTestFilerClient{
|
||||
entriesByDir: map[string][]*filer_pb.Entry{"/root": {}},
|
||||
}
|
||||
clientB := &verifyTestFilerClient{
|
||||
entriesByDir: map[string][]*filer_pb.Entry{
|
||||
"/root": {verifyFileEntry("extra.txt", 50)},
|
||||
},
|
||||
}
|
||||
|
||||
t.Run("bidirectional", func(t *testing.T) {
|
||||
result := &VerifyResult{}
|
||||
sem := make(chan struct{}, verifySyncConcurrency)
|
||||
if err := compareDirectory(context.Background(), clientA, clientB,
|
||||
"/root", "/root", false, time.Time{}, sem, result); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if got := result.onlyInB.Load(); got != 1 {
|
||||
t.Errorf("onlyInB = %d, want 1", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("active-passive ignores onlyInB", func(t *testing.T) {
|
||||
result := &VerifyResult{}
|
||||
sem := make(chan struct{}, verifySyncConcurrency)
|
||||
if err := compareDirectory(context.Background(), clientA, clientB,
|
||||
"/root", "/root", true, time.Time{}, sem, result); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if got := result.onlyInB.Load(); got != 0 {
|
||||
t.Errorf("onlyInB = %d, want 0 in active-passive mode", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestVerifySyncSizeMismatch confirms that a file with differing sizes is
|
||||
// counted as a size mismatch and not as missing.
|
||||
func TestVerifySyncSizeMismatch(t *testing.T) {
|
||||
clientA := &verifyTestFilerClient{
|
||||
entriesByDir: map[string][]*filer_pb.Entry{
|
||||
"/root": {verifyFileEntry("data.bin", 1024)},
|
||||
},
|
||||
}
|
||||
clientB := &verifyTestFilerClient{
|
||||
entriesByDir: map[string][]*filer_pb.Entry{
|
||||
"/root": {verifyFileEntry("data.bin", 512)},
|
||||
},
|
||||
}
|
||||
|
||||
result := &VerifyResult{}
|
||||
sem := make(chan struct{}, verifySyncConcurrency)
|
||||
err := compareDirectory(context.Background(), clientA, clientB,
|
||||
"/root", "/root", false, time.Time{}, sem, result)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if got := result.sizeMismatch.Load(); got != 1 {
|
||||
t.Errorf("sizeMismatch = %d, want 1", got)
|
||||
}
|
||||
if got := result.missingCount.Load(); got != 0 {
|
||||
t.Errorf("missingCount = %d, want 0", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestVerifySyncConcurrencyBound verifies that the shared semaphore keeps peak
|
||||
// concurrent filer listings at or below verifySyncConcurrency at all times.
|
||||
// A 5ms delay per WithFilerClient call makes the concurrency overlap observable.
|
||||
func TestVerifySyncConcurrencyBound(t *testing.T) {
|
||||
// Wide, shallow tree: root with 20 identical subdirectories.
|
||||
const fanout = 20
|
||||
entriesA := make(map[string][]*filer_pb.Entry)
|
||||
entriesB := make(map[string][]*filer_pb.Entry)
|
||||
|
||||
rootDirs := make([]*filer_pb.Entry, fanout)
|
||||
for i := range fanout {
|
||||
name := fmt.Sprintf("sub%02d", i)
|
||||
rootDirs[i] = verifyDirEntry(name)
|
||||
entriesA["/root/"+name] = []*filer_pb.Entry{verifyFileEntry("f.txt", 10)}
|
||||
entriesB["/root/"+name] = []*filer_pb.Entry{verifyFileEntry("f.txt", 10)}
|
||||
}
|
||||
entriesA["/root"] = rootDirs
|
||||
entriesB["/root"] = rootDirs
|
||||
|
||||
clientA := &verifyTestFilerClient{entriesByDir: entriesA, delay: 5 * time.Millisecond}
|
||||
clientB := &verifyTestFilerClient{entriesByDir: entriesB, delay: 5 * time.Millisecond}
|
||||
|
||||
result := &VerifyResult{}
|
||||
sem := make(chan struct{}, verifySyncConcurrency)
|
||||
if err := compareDirectory(context.Background(), clientA, clientB,
|
||||
"/root", "/root", false, time.Time{}, sem, result); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if result.missingCount.Load() != 0 || result.sizeMismatch.Load() != 0 {
|
||||
t.Errorf("unexpected diffs in identical tree")
|
||||
}
|
||||
|
||||
if peak := clientA.peakFlight.Load(); peak > verifySyncConcurrency {
|
||||
t.Errorf("clientA peak concurrent listings = %d, want ≤ %d (verifySyncConcurrency)",
|
||||
peak, verifySyncConcurrency)
|
||||
}
|
||||
if peak := clientB.peakFlight.Load(); peak > verifySyncConcurrency {
|
||||
t.Errorf("clientB peak concurrent listings = %d, want ≤ %d (verifySyncConcurrency)",
|
||||
peak, verifySyncConcurrency)
|
||||
}
|
||||
}
|
||||
|
||||
// TestVerifySyncETagMismatch confirms that two files with the same size but
|
||||
// different Md5 checksums are counted as an ETag mismatch, not a size mismatch.
|
||||
func TestVerifySyncETagMismatch(t *testing.T) {
|
||||
newEntry := func(name string, md5 []byte) *filer_pb.Entry {
|
||||
return &filer_pb.Entry{
|
||||
Name: name,
|
||||
Attributes: &filer_pb.FuseAttributes{
|
||||
FileSize: 100,
|
||||
Md5: md5,
|
||||
},
|
||||
}
|
||||
}
|
||||
clientA := &verifyTestFilerClient{
|
||||
entriesByDir: map[string][]*filer_pb.Entry{
|
||||
"/root": {newEntry("data.bin", []byte{0x11, 0x22, 0x33})},
|
||||
},
|
||||
}
|
||||
clientB := &verifyTestFilerClient{
|
||||
entriesByDir: map[string][]*filer_pb.Entry{
|
||||
"/root": {newEntry("data.bin", []byte{0x44, 0x55, 0x66})},
|
||||
},
|
||||
}
|
||||
|
||||
result := &VerifyResult{}
|
||||
sem := make(chan struct{}, verifySyncConcurrency)
|
||||
if err := compareDirectory(context.Background(), clientA, clientB,
|
||||
"/root", "/root", false, time.Time{}, sem, result); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if got := result.etagMismatch.Load(); got != 1 {
|
||||
t.Errorf("etagMismatch = %d, want 1", got)
|
||||
}
|
||||
if got := result.sizeMismatch.Load(); got != 0 {
|
||||
t.Errorf("sizeMismatch = %d, want 0 (same size should not trigger size mismatch)", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestVerifySyncChunkReorder confirms that two entries holding the SAME
|
||||
// chunks (same offsets, same per-chunk MD5s) in a DIFFERENT slice order are
|
||||
// classified as CHUNK_REORDER (content-equal), not ETAG_MISMATCH, and are
|
||||
// therefore not counted as errors. This is the S3-multipart vs filer.backup
|
||||
// reordering false positive.
|
||||
func TestVerifySyncChunkReorder(t *testing.T) {
|
||||
c0 := verifyChunk(0, 100, "chunk-0")
|
||||
c1 := verifyChunk(100, 100, "chunk-1")
|
||||
c2 := verifyChunk(200, 100, "chunk-2")
|
||||
|
||||
// A stores [c0,c1,c2]; B stores a permutation [c2,c0,c1] of the same chunks.
|
||||
clientA := &verifyTestFilerClient{
|
||||
entriesByDir: map[string][]*filer_pb.Entry{
|
||||
"/root": {verifyChunkedEntry("model.bin", []*filer_pb.FileChunk{c0, c1, c2})},
|
||||
},
|
||||
}
|
||||
clientB := &verifyTestFilerClient{
|
||||
entriesByDir: map[string][]*filer_pb.Entry{
|
||||
"/root": {verifyChunkedEntry("model.bin", []*filer_pb.FileChunk{c2, c0, c1})},
|
||||
},
|
||||
}
|
||||
|
||||
result := &VerifyResult{}
|
||||
sem := make(chan struct{}, verifySyncConcurrency)
|
||||
if err := compareDirectory(context.Background(), clientA, clientB,
|
||||
"/root", "/root", false, time.Time{}, sem, result); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if got := result.chunkReorder.Load(); got != 1 {
|
||||
t.Errorf("chunkReorder = %d, want 1", got)
|
||||
}
|
||||
if got := result.etagMismatch.Load(); got != 0 {
|
||||
t.Errorf("etagMismatch = %d, want 0 (reordered chunks are content-equal)", got)
|
||||
}
|
||||
if got := result.sizeMismatch.Load(); got != 0 {
|
||||
t.Errorf("sizeMismatch = %d, want 0", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestVerifySyncGenuineChunkDivergence confirms that when one chunk's content
|
||||
// actually differs (same offsets and count, different per-chunk MD5), the file
|
||||
// stays classified as ETAG_MISMATCH and is NOT downgraded to CHUNK_REORDER.
|
||||
func TestVerifySyncGenuineChunkDivergence(t *testing.T) {
|
||||
clientA := &verifyTestFilerClient{
|
||||
entriesByDir: map[string][]*filer_pb.Entry{
|
||||
"/root": {verifyChunkedEntry("model.bin", []*filer_pb.FileChunk{
|
||||
verifyChunk(0, 100, "chunk-0"),
|
||||
verifyChunk(100, 100, "chunk-1"),
|
||||
verifyChunk(200, 100, "chunk-2"),
|
||||
})},
|
||||
},
|
||||
}
|
||||
clientB := &verifyTestFilerClient{
|
||||
entriesByDir: map[string][]*filer_pb.Entry{
|
||||
"/root": {verifyChunkedEntry("model.bin", []*filer_pb.FileChunk{
|
||||
verifyChunk(0, 100, "chunk-0"),
|
||||
verifyChunk(100, 100, "chunk-1"),
|
||||
verifyChunk(200, 100, "chunk-2-DIFFERENT"), // real content divergence
|
||||
})},
|
||||
},
|
||||
}
|
||||
|
||||
result := &VerifyResult{}
|
||||
sem := make(chan struct{}, verifySyncConcurrency)
|
||||
if err := compareDirectory(context.Background(), clientA, clientB,
|
||||
"/root", "/root", false, time.Time{}, sem, result); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if got := result.etagMismatch.Load(); got != 1 {
|
||||
t.Errorf("etagMismatch = %d, want 1 (genuine chunk content divergence)", got)
|
||||
}
|
||||
if got := result.chunkReorder.Load(); got != 0 {
|
||||
t.Errorf("chunkReorder = %d, want 0 (content actually differs)", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestVerifySyncChunkCountDiffStaysEtagMismatch confirms that a differing chunk
|
||||
// count with an equal file size (e.g. differently split content) is NOT treated
|
||||
// as a reordering — it stays ETAG_MISMATCH.
|
||||
func TestVerifySyncChunkCountDiffStaysEtagMismatch(t *testing.T) {
|
||||
clientA := &verifyTestFilerClient{
|
||||
entriesByDir: map[string][]*filer_pb.Entry{
|
||||
"/root": {verifyChunkedEntry("data.bin", []*filer_pb.FileChunk{
|
||||
verifyChunk(0, 100, "a"),
|
||||
verifyChunk(100, 100, "b"),
|
||||
verifyChunk(200, 100, "c"),
|
||||
})},
|
||||
},
|
||||
}
|
||||
clientB := &verifyTestFilerClient{
|
||||
entriesByDir: map[string][]*filer_pb.Entry{
|
||||
"/root": {verifyChunkedEntry("data.bin", []*filer_pb.FileChunk{
|
||||
verifyChunk(0, 150, "x"),
|
||||
verifyChunk(150, 150, "y"),
|
||||
})},
|
||||
},
|
||||
}
|
||||
|
||||
result := &VerifyResult{}
|
||||
sem := make(chan struct{}, verifySyncConcurrency)
|
||||
if err := compareDirectory(context.Background(), clientA, clientB,
|
||||
"/root", "/root", false, time.Time{}, sem, result); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if got := result.sizeMismatch.Load(); got != 0 {
|
||||
t.Errorf("sizeMismatch = %d, want 0 (both total 300 bytes)", got)
|
||||
}
|
||||
if got := result.etagMismatch.Load(); got != 1 {
|
||||
t.Errorf("etagMismatch = %d, want 1", got)
|
||||
}
|
||||
if got := result.chunkReorder.Load(); got != 0 {
|
||||
t.Errorf("chunkReorder = %d, want 0", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestVerifySyncDuplicateOffsetStaysEtagMismatch confirms that overlapping
|
||||
// chunks at the same offset — where the visible bytes are resolved by chunk
|
||||
// timestamp, not by the raw chunk list — are NOT fast-pathed to CHUNK_REORDER.
|
||||
// Both sides hold the same two ETags at offset 0 in a different order, so the
|
||||
// file ETags differ; the visible content is ambiguous from the list alone, so
|
||||
// this must stay ETAG_MISMATCH.
|
||||
func TestVerifySyncDuplicateOffsetStaysEtagMismatch(t *testing.T) {
|
||||
c1 := verifyChunk(0, 100, "v1")
|
||||
c2 := verifyChunk(0, 100, "v2") // same offset → overlap
|
||||
|
||||
clientA := &verifyTestFilerClient{
|
||||
entriesByDir: map[string][]*filer_pb.Entry{
|
||||
"/root": {verifyChunkedEntry("data.bin", []*filer_pb.FileChunk{c1, c2})},
|
||||
},
|
||||
}
|
||||
clientB := &verifyTestFilerClient{
|
||||
entriesByDir: map[string][]*filer_pb.Entry{
|
||||
"/root": {verifyChunkedEntry("data.bin", []*filer_pb.FileChunk{c2, c1})},
|
||||
},
|
||||
}
|
||||
|
||||
result := &VerifyResult{}
|
||||
sem := make(chan struct{}, verifySyncConcurrency)
|
||||
if err := compareDirectory(context.Background(), clientA, clientB,
|
||||
"/root", "/root", false, time.Time{}, sem, result); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if got := result.etagMismatch.Load(); got != 1 {
|
||||
t.Errorf("etagMismatch = %d, want 1 (overlapping offsets are not a safe reorder)", got)
|
||||
}
|
||||
if got := result.chunkReorder.Load(); got != 0 {
|
||||
t.Errorf("chunkReorder = %d, want 0", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestVerifySyncManifestChunkStaysEtagMismatch confirms that entries containing
|
||||
// a manifest chunk are not fast-pathed to CHUNK_REORDER: manifest chunks
|
||||
// represent compacted, possibly overlapping history that this check does not
|
||||
// resolve.
|
||||
func TestVerifySyncManifestChunkStaysEtagMismatch(t *testing.T) {
|
||||
manifest := func(offset int64, size uint64, seed string) *filer_pb.FileChunk {
|
||||
c := verifyChunk(offset, size, seed)
|
||||
c.IsChunkManifest = true
|
||||
return c
|
||||
}
|
||||
a0, a1 := manifest(0, 100, "m0"), verifyChunk(100, 100, "m1")
|
||||
clientA := &verifyTestFilerClient{
|
||||
entriesByDir: map[string][]*filer_pb.Entry{
|
||||
"/root": {verifyChunkedEntry("data.bin", []*filer_pb.FileChunk{a0, a1})},
|
||||
},
|
||||
}
|
||||
clientB := &verifyTestFilerClient{
|
||||
entriesByDir: map[string][]*filer_pb.Entry{
|
||||
"/root": {verifyChunkedEntry("data.bin", []*filer_pb.FileChunk{a1, a0})},
|
||||
},
|
||||
}
|
||||
|
||||
result := &VerifyResult{}
|
||||
sem := make(chan struct{}, verifySyncConcurrency)
|
||||
if err := compareDirectory(context.Background(), clientA, clientB,
|
||||
"/root", "/root", false, time.Time{}, sem, result); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if got := result.etagMismatch.Load(); got != 1 {
|
||||
t.Errorf("etagMismatch = %d, want 1 (manifest chunks are not fast-pathed)", got)
|
||||
}
|
||||
if got := result.chunkReorder.Load(); got != 0 {
|
||||
t.Errorf("chunkReorder = %d, want 0", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestVerifySyncEmptyChunkETagStaysEtagMismatch confirms that a chunk with an
|
||||
// empty per-chunk ETag is never fast-pathed to CHUNK_REORDER: an empty ETag is
|
||||
// not a content fingerprint, so (offset, size, ETag) equality cannot prove the
|
||||
// bytes match. The two sides reorder their non-empty chunks so the file ETags
|
||||
// differ and the reorder check is reached, but the empty-ETag chunk in the
|
||||
// middle must keep it ETAG_MISMATCH rather than assert a false content-equality.
|
||||
func TestVerifySyncEmptyChunkETagStaysEtagMismatch(t *testing.T) {
|
||||
c0 := verifyChunk(0, 100, "e0")
|
||||
empty := &filer_pb.FileChunk{Offset: 100, Size: 100} // no ETag → not a fingerprint
|
||||
c2 := verifyChunk(200, 100, "e2")
|
||||
|
||||
clientA := &verifyTestFilerClient{
|
||||
entriesByDir: map[string][]*filer_pb.Entry{
|
||||
"/root": {verifyChunkedEntry("data.bin", []*filer_pb.FileChunk{c0, empty, c2})},
|
||||
},
|
||||
}
|
||||
clientB := &verifyTestFilerClient{
|
||||
entriesByDir: map[string][]*filer_pb.Entry{
|
||||
"/root": {verifyChunkedEntry("data.bin", []*filer_pb.FileChunk{c2, c0, empty})},
|
||||
},
|
||||
}
|
||||
|
||||
result := &VerifyResult{}
|
||||
sem := make(chan struct{}, verifySyncConcurrency)
|
||||
if err := compareDirectory(context.Background(), clientA, clientB,
|
||||
"/root", "/root", false, time.Time{}, sem, result); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if got := result.etagMismatch.Load(); got != 1 {
|
||||
t.Errorf("etagMismatch = %d, want 1 (empty per-chunk ETag is not a content fingerprint)", got)
|
||||
}
|
||||
if got := result.chunkReorder.Load(); got != 0 {
|
||||
t.Errorf("chunkReorder = %d, want 0", got)
|
||||
}
|
||||
}
|
||||
|
||||
// captureStdout runs fn with os.Stdout redirected to a pipe and returns what it
|
||||
// wrote. Tests that assert on emitted diff records use it.
|
||||
func captureStdout(t *testing.T, fn func()) string {
|
||||
t.Helper()
|
||||
orig := os.Stdout
|
||||
r, w, err := os.Pipe()
|
||||
if err != nil {
|
||||
t.Fatalf("os.Pipe: %v", err)
|
||||
}
|
||||
os.Stdout = w
|
||||
fn()
|
||||
w.Close()
|
||||
os.Stdout = orig
|
||||
data, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
t.Fatalf("read captured stdout: %v", err)
|
||||
}
|
||||
return string(data)
|
||||
}
|
||||
|
||||
// TestVerifySyncChunkReorderJSONEmittedByDefault confirms that in JSON output
|
||||
// mode a CHUNK_REORDER record is emitted at default verbosity: the -v=1 gate
|
||||
// suppresses only the human text report, so the NDJSON per-record stream stays
|
||||
// consistent with the summary count a machine consumer reads.
|
||||
func TestVerifySyncChunkReorderJSONEmittedByDefault(t *testing.T) {
|
||||
c0 := verifyChunk(0, 100, "chunk-0")
|
||||
c1 := verifyChunk(100, 100, "chunk-1")
|
||||
entryA := verifyChunkedEntry("model.bin", []*filer_pb.FileChunk{c0, c1})
|
||||
entryB := verifyChunkedEntry("model.bin", []*filer_pb.FileChunk{c1, c0})
|
||||
|
||||
result := &VerifyResult{jsonOutput: true}
|
||||
out := captureStdout(t, func() {
|
||||
reportDiff(diffChunkReorder, "/root", entryA, entryB, result)
|
||||
})
|
||||
|
||||
if got := result.chunkReorder.Load(); got != 1 {
|
||||
t.Fatalf("chunkReorder = %d, want 1", got)
|
||||
}
|
||||
if !strings.Contains(out, `"type":"CHUNK_REORDER"`) {
|
||||
t.Errorf("JSON output missing CHUNK_REORDER record at default verbosity; got %q", out)
|
||||
}
|
||||
}
|
||||
|
||||
// TestVerifySyncCutoffTime verifies that entries newer than cutoffTime are
|
||||
// skipped in both the A-only (MISSING) and B-only (ONLY_IN_B) branches.
|
||||
func TestVerifySyncCutoffTime(t *testing.T) {
|
||||
cutoff := time.Unix(1000, 0)
|
||||
|
||||
recentEntry := func(name string) *filer_pb.Entry {
|
||||
return &filer_pb.Entry{
|
||||
Name: name,
|
||||
Attributes: &filer_pb.FuseAttributes{FileSize: 10, Mtime: 2000}, // > cutoff
|
||||
}
|
||||
}
|
||||
oldEntry := func(name string) *filer_pb.Entry {
|
||||
return &filer_pb.Entry{
|
||||
Name: name,
|
||||
Attributes: &filer_pb.FuseAttributes{FileSize: 10, Mtime: 500}, // < cutoff
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("A-only recent file is skipped, not reported missing", func(t *testing.T) {
|
||||
clientA := &verifyTestFilerClient{
|
||||
entriesByDir: map[string][]*filer_pb.Entry{"/": {recentEntry("new.txt")}},
|
||||
}
|
||||
clientB := &verifyTestFilerClient{
|
||||
entriesByDir: map[string][]*filer_pb.Entry{"/": {}},
|
||||
}
|
||||
result := &VerifyResult{}
|
||||
sem := make(chan struct{}, verifySyncConcurrency)
|
||||
if err := compareDirectory(context.Background(), clientA, clientB,
|
||||
"/", "/", false, cutoff, sem, result); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if got := result.skippedRecent.Load(); got != 1 {
|
||||
t.Errorf("skippedRecent = %d, want 1", got)
|
||||
}
|
||||
if got := result.missingCount.Load(); got != 0 {
|
||||
t.Errorf("missingCount = %d, want 0 (recent file should be skipped)", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("A-only old file is reported missing", func(t *testing.T) {
|
||||
clientA := &verifyTestFilerClient{
|
||||
entriesByDir: map[string][]*filer_pb.Entry{"/": {oldEntry("old.txt")}},
|
||||
}
|
||||
clientB := &verifyTestFilerClient{
|
||||
entriesByDir: map[string][]*filer_pb.Entry{"/": {}},
|
||||
}
|
||||
result := &VerifyResult{}
|
||||
sem := make(chan struct{}, verifySyncConcurrency)
|
||||
if err := compareDirectory(context.Background(), clientA, clientB,
|
||||
"/", "/", false, cutoff, sem, result); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if got := result.missingCount.Load(); got != 1 {
|
||||
t.Errorf("missingCount = %d, want 1", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("B-only recent file is skipped, not reported as ONLY_IN_B", func(t *testing.T) {
|
||||
clientA := &verifyTestFilerClient{
|
||||
entriesByDir: map[string][]*filer_pb.Entry{"/": {}},
|
||||
}
|
||||
clientB := &verifyTestFilerClient{
|
||||
entriesByDir: map[string][]*filer_pb.Entry{"/": {recentEntry("new.txt")}},
|
||||
}
|
||||
result := &VerifyResult{}
|
||||
sem := make(chan struct{}, verifySyncConcurrency)
|
||||
if err := compareDirectory(context.Background(), clientA, clientB,
|
||||
"/", "/", false, cutoff, sem, result); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if got := result.skippedRecent.Load(); got != 1 {
|
||||
t.Errorf("skippedRecent = %d, want 1", got)
|
||||
}
|
||||
if got := result.onlyInB.Load(); got != 0 {
|
||||
t.Errorf("onlyInB = %d, want 0 (recent B-only file should be skipped)", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("B-only old file is reported as ONLY_IN_B", func(t *testing.T) {
|
||||
clientA := &verifyTestFilerClient{
|
||||
entriesByDir: map[string][]*filer_pb.Entry{"/": {}},
|
||||
}
|
||||
clientB := &verifyTestFilerClient{
|
||||
entriesByDir: map[string][]*filer_pb.Entry{"/": {oldEntry("old.txt")}},
|
||||
}
|
||||
result := &VerifyResult{}
|
||||
sem := make(chan struct{}, verifySyncConcurrency)
|
||||
if err := compareDirectory(context.Background(), clientA, clientB,
|
||||
"/", "/", false, cutoff, sem, result); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if got := result.onlyInB.Load(); got != 1 {
|
||||
t.Errorf("onlyInB = %d, want 1", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestVerifySyncCutoffMatchedFileBSideRecent verifies that when matched-name
|
||||
// files differ but only the B side is recently modified, the comparison is
|
||||
// skipped (sync-lag tolerance) rather than reporting a spurious mismatch.
|
||||
func TestVerifySyncCutoffMatchedFileBSideRecent(t *testing.T) {
|
||||
cutoff := time.Unix(1000, 0)
|
||||
|
||||
entry := func(size uint64, mtime int64) *filer_pb.Entry {
|
||||
return &filer_pb.Entry{
|
||||
Name: "data.bin",
|
||||
Attributes: &filer_pb.FuseAttributes{FileSize: size, Mtime: mtime},
|
||||
}
|
||||
}
|
||||
|
||||
// A is old (size 100), B is recently rewritten with a different size.
|
||||
// Without the B-side cutoff check this would surface as SIZE_MISMATCH.
|
||||
clientA := &verifyTestFilerClient{
|
||||
entriesByDir: map[string][]*filer_pb.Entry{"/": {entry(100, 500)}},
|
||||
}
|
||||
clientB := &verifyTestFilerClient{
|
||||
entriesByDir: map[string][]*filer_pb.Entry{"/": {entry(200, 2000)}},
|
||||
}
|
||||
|
||||
result := &VerifyResult{}
|
||||
sem := make(chan struct{}, verifySyncConcurrency)
|
||||
if err := compareDirectory(context.Background(), clientA, clientB,
|
||||
"/", "/", false, cutoff, sem, result); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if got := result.skippedRecent.Load(); got != 1 {
|
||||
t.Errorf("skippedRecent = %d, want 1 (B-side recent should skip)", got)
|
||||
}
|
||||
if got := result.sizeMismatch.Load(); got != 0 {
|
||||
t.Errorf("sizeMismatch = %d, want 0 (recent B should not surface as mismatch)", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestVerifySyncMissingDirRecursesEvenWithRecentMtime verifies that a
|
||||
// directory missing in B with a recent mtime still has its subtree walked,
|
||||
// so older missing files inside are reported. A recent child write can bump
|
||||
// the parent mtime even though older missing files exist underneath.
|
||||
func TestVerifySyncMissingDirRecursesEvenWithRecentMtime(t *testing.T) {
|
||||
cutoff := time.Unix(1000, 0)
|
||||
|
||||
recentDir := &filer_pb.Entry{
|
||||
Name: "subdir",
|
||||
IsDirectory: true,
|
||||
Attributes: &filer_pb.FuseAttributes{Mtime: 2000}, // > cutoff
|
||||
}
|
||||
oldChild := &filer_pb.Entry{
|
||||
Name: "old.txt",
|
||||
Attributes: &filer_pb.FuseAttributes{FileSize: 10, Mtime: 500}, // < cutoff
|
||||
}
|
||||
|
||||
clientA := &verifyTestFilerClient{
|
||||
entriesByDir: map[string][]*filer_pb.Entry{
|
||||
"/": {recentDir},
|
||||
"/subdir": {oldChild},
|
||||
},
|
||||
}
|
||||
clientB := &verifyTestFilerClient{
|
||||
entriesByDir: map[string][]*filer_pb.Entry{
|
||||
"/": {},
|
||||
},
|
||||
}
|
||||
|
||||
result := &VerifyResult{}
|
||||
sem := make(chan struct{}, verifySyncConcurrency)
|
||||
if err := compareDirectory(context.Background(), clientA, clientB,
|
||||
"/", "/", false, cutoff, sem, result); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
// Expect: directory MISSING + recursed-old-file MISSING = 2 missing.
|
||||
if got := result.missingCount.Load(); got != 2 {
|
||||
t.Errorf("missingCount = %d, want 2 (recent dir + old child inside)", got)
|
||||
}
|
||||
if got := result.skippedRecent.Load(); got != 0 {
|
||||
t.Errorf("skippedRecent = %d, want 0 (dir mtime should not gate recursion)", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestVerifySyncRootPath is a regression test for the path.Join fix.
|
||||
// fmt.Sprintf("%s/%s", "/", name) produced "//name"; path.Join produces "/name".
|
||||
// This test walks from "/" and verifies the child directory is found and
|
||||
// compared correctly (not silently skipped due to a malformed path).
|
||||
func TestVerifySyncRootPath(t *testing.T) {
|
||||
clientA := &verifyTestFilerClient{
|
||||
entriesByDir: map[string][]*filer_pb.Entry{
|
||||
"/": {verifyDirEntry("data")},
|
||||
"/data": {verifyFileEntry("file.txt", 42)},
|
||||
},
|
||||
}
|
||||
clientB := &verifyTestFilerClient{
|
||||
entriesByDir: map[string][]*filer_pb.Entry{
|
||||
"/": {verifyDirEntry("data")},
|
||||
"/data": {verifyFileEntry("file.txt", 42)},
|
||||
},
|
||||
}
|
||||
|
||||
result := &VerifyResult{}
|
||||
sem := make(chan struct{}, verifySyncConcurrency)
|
||||
if err := compareDirectory(context.Background(), clientA, clientB,
|
||||
"/", "/", false, time.Time{}, sem, result); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if result.missingCount.Load() != 0 || result.sizeMismatch.Load() != 0 {
|
||||
t.Errorf("identical trees from root should have no diffs: missing=%d size=%d",
|
||||
result.missingCount.Load(), result.sizeMismatch.Load())
|
||||
}
|
||||
// 2 directories traversed: "/" and "/data"
|
||||
if got := result.dirCount.Load(); got != 2 {
|
||||
t.Errorf("dirCount = %d, want 2 (root + /data)", got)
|
||||
}
|
||||
// 1 file compared: /data/file.txt
|
||||
if got := result.fileCount.Load(); got != 1 {
|
||||
t.Errorf("fileCount = %d, want 1", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestVerifySyncNoDeadlockDeepTree ensures that a tree deeper than
|
||||
// verifySyncConcurrency completes without deadlocking. With a per-call
|
||||
// semaphore the walk would still complete (just with unbounded goroutines);
|
||||
// this test mainly guards that the shared-semaphore release-before-recurse
|
||||
// invariant holds — i.e. the walk finishes within the timeout.
|
||||
func TestVerifySyncNoDeadlockDeepTree(t *testing.T) {
|
||||
// Build a binary tree of depth 10 (well past verifySyncConcurrency=5).
|
||||
const depth = 10
|
||||
entriesA := make(map[string][]*filer_pb.Entry)
|
||||
entriesB := make(map[string][]*filer_pb.Entry)
|
||||
|
||||
var buildTree func(path string, d int)
|
||||
buildTree = func(path string, d int) {
|
||||
if d == 0 {
|
||||
entriesA[path] = []*filer_pb.Entry{verifyFileEntry("leaf.txt", 1)}
|
||||
entriesB[path] = []*filer_pb.Entry{verifyFileEntry("leaf.txt", 1)}
|
||||
return
|
||||
}
|
||||
children := []*filer_pb.Entry{verifyDirEntry("left"), verifyDirEntry("right")}
|
||||
entriesA[path] = children
|
||||
entriesB[path] = children
|
||||
buildTree(path+"/left", d-1)
|
||||
buildTree(path+"/right", d-1)
|
||||
}
|
||||
buildTree("/root", depth)
|
||||
|
||||
clientA := &verifyTestFilerClient{entriesByDir: entriesA}
|
||||
clientB := &verifyTestFilerClient{entriesByDir: entriesB}
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
result := &VerifyResult{}
|
||||
sem := make(chan struct{}, verifySyncConcurrency)
|
||||
done <- compareDirectory(context.Background(), clientA, clientB,
|
||||
"/root", "/root", false, time.Time{}, sem, result)
|
||||
}()
|
||||
|
||||
select {
|
||||
case err := <-done:
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
case <-time.After(10 * time.Second):
|
||||
t.Fatal("compareDirectory did not complete within 10s — possible deadlock")
|
||||
}
|
||||
}
|
||||
|
||||
// TestVerifySyncByteOrderSkew covers a collation skew between two filer
|
||||
// versions: both return the SAME name set in DIFFERENT order. Filer A (older,
|
||||
// pre-4.32) lists in locale collation (case-insensitive: lowercase mixes in
|
||||
// among uppercase); filer B (4.32+, with PR #9824) lists in byte order
|
||||
// (uppercase before lowercase). The mock returns each side's slice verbatim.
|
||||
//
|
||||
// Because compareDirectory sorts both sides client-side before merging, the two
|
||||
// orders converge and no spurious diffs are reported. Were the sort removed, the
|
||||
// streaming merge would desync and count 3 false MISSING + 3 false ONLY_IN_B.
|
||||
func TestVerifySyncByteOrderSkew(t *testing.T) {
|
||||
// locale order (case-insensitive): lowercase mixes in among uppercase.
|
||||
localeOrder := []*filer_pb.Entry{
|
||||
verifyFileEntry("sk-4", 10),
|
||||
verifyFileEntry("sk-8", 10),
|
||||
verifyFileEntry("sk-mmsJ", 10),
|
||||
verifyFileEntry("sk-nFGE", 10),
|
||||
verifyFileEntry("sk-RH0Z", 10),
|
||||
verifyFileEntry("sk-Xp", 10),
|
||||
verifyFileEntry("sk-Z06", 10),
|
||||
}
|
||||
// byte order: uppercase (R,X,Z) sort before lowercase (m,n).
|
||||
byteOrder := []*filer_pb.Entry{
|
||||
verifyFileEntry("sk-4", 10),
|
||||
verifyFileEntry("sk-8", 10),
|
||||
verifyFileEntry("sk-RH0Z", 10),
|
||||
verifyFileEntry("sk-Xp", 10),
|
||||
verifyFileEntry("sk-Z06", 10),
|
||||
verifyFileEntry("sk-mmsJ", 10),
|
||||
verifyFileEntry("sk-nFGE", 10),
|
||||
}
|
||||
|
||||
clientA := &verifyTestFilerClient{
|
||||
entriesByDir: map[string][]*filer_pb.Entry{"/root": localeOrder},
|
||||
}
|
||||
clientB := &verifyTestFilerClient{
|
||||
entriesByDir: map[string][]*filer_pb.Entry{"/root": byteOrder},
|
||||
}
|
||||
|
||||
result := &VerifyResult{}
|
||||
sem := make(chan struct{}, verifySyncConcurrency)
|
||||
if err := compareDirectory(context.Background(), clientA, clientB,
|
||||
"/root", "/root", false, time.Time{}, sem, result); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if got := result.missingCount.Load(); got != 0 {
|
||||
t.Errorf("missingCount = %d, want 0", got)
|
||||
}
|
||||
if got := result.onlyInB.Load(); got != 0 {
|
||||
t.Errorf("onlyInB = %d, want 0", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestVerifySyncSourcesListConcurrently guards against re-serializing the two
|
||||
// directory listings: both sides buffer + sort, and their loads must run
|
||||
// concurrently — not A fully then B. A shared gate releases only once both
|
||||
// listings are in-flight at the same time; if they were sequential the first
|
||||
// would block on the gate and time out.
|
||||
func TestVerifySyncSourcesListConcurrently(t *testing.T) {
|
||||
var started atomic.Int32
|
||||
var timedOut atomic.Bool
|
||||
release := make(chan struct{})
|
||||
gate := func() {
|
||||
if started.Add(1) == 2 {
|
||||
close(release) // both listings reached the gate → proceed
|
||||
}
|
||||
select {
|
||||
case <-release:
|
||||
case <-time.After(3 * time.Second):
|
||||
timedOut.Store(true) // only one listing ever in-flight → serialized
|
||||
}
|
||||
}
|
||||
|
||||
entries := []*filer_pb.Entry{verifyFileEntry("a", 1), verifyFileEntry("b", 1)}
|
||||
clientA := &verifyTestFilerClient{
|
||||
entriesByDir: map[string][]*filer_pb.Entry{"/root": entries},
|
||||
onList: gate,
|
||||
}
|
||||
clientB := &verifyTestFilerClient{
|
||||
entriesByDir: map[string][]*filer_pb.Entry{"/root": entries},
|
||||
onList: gate,
|
||||
}
|
||||
|
||||
result := &VerifyResult{}
|
||||
sem := make(chan struct{}, verifySyncConcurrency)
|
||||
if err := compareDirectory(context.Background(), clientA, clientB,
|
||||
"/root", "/root", false, time.Time{}, sem, result); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if timedOut.Load() {
|
||||
t.Fatal("sources listed sequentially: both listings were not in-flight at once")
|
||||
}
|
||||
}
|
||||
|
||||
// TestVerifySyncListErrorNoBogusDiffs verifies that a listing failure aborts
|
||||
// with the error and reports no differences. A side whose listing errors keeps
|
||||
// only a partial, unsorted buffer; the error must be surfaced before the merge
|
||||
// so those partial entries never produce spurious MISSING / ONLY_IN_B.
|
||||
func TestVerifySyncListErrorNoBogusDiffs(t *testing.T) {
|
||||
entries := []*filer_pb.Entry{
|
||||
verifyFileEntry("a", 1),
|
||||
verifyFileEntry("b", 1),
|
||||
verifyFileEntry("c", 1),
|
||||
}
|
||||
// A errors mid-listing (partial buffer); B lists cleanly.
|
||||
clientA := &verifyTestFilerClient{
|
||||
entriesByDir: map[string][]*filer_pb.Entry{"/root": entries},
|
||||
recvErr: fmt.Errorf("injected list failure"),
|
||||
}
|
||||
clientB := &verifyTestFilerClient{
|
||||
entriesByDir: map[string][]*filer_pb.Entry{"/root": entries},
|
||||
}
|
||||
|
||||
result := &VerifyResult{}
|
||||
sem := make(chan struct{}, verifySyncConcurrency)
|
||||
err := compareDirectory(context.Background(), clientA, clientB,
|
||||
"/root", "/root", false, time.Time{}, sem, result)
|
||||
if err == nil {
|
||||
t.Fatal("expected a listing error, got nil")
|
||||
}
|
||||
if got := result.missingCount.Load(); got != 0 {
|
||||
t.Errorf("missingCount = %d, want 0 (no bogus diffs on list error)", got)
|
||||
}
|
||||
if got := result.onlyInB.Load(); got != 0 {
|
||||
t.Errorf("onlyInB = %d, want 0 (no bogus diffs on list error)", got)
|
||||
}
|
||||
if got := result.fileCount.Load(); got != 0 {
|
||||
t.Errorf("fileCount = %d, want 0 (merge must not run on failed listing)", got)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user