Files
upstream-seaweedfs/weed/topology/collection_statistics_test.go
upstream-mirror 89f5be005d
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
snapshot of https://github.com/seaweedfs/seaweedfs.git (master, 2026-09-16; blobs > 131072B omitted)
2026-09-16 03:23:42 +05:00

310 lines
12 KiB
Go

package topology
import (
"testing"
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
"github.com/seaweedfs/seaweedfs/weed/storage/erasure_coding"
)
// referenceCollectionStatistics is what callers computed for themselves from a
// full topology listing, kept here so the summary can be held to producing the
// same numbers. Quota enforcement reads these.
func referenceCollectionStatistics(t *master_pb.TopologyInfo) map[string]*CollectionStatistics {
type volumeKey struct {
collection string
volumeId uint32
}
out := map[string]*CollectionStatistics{}
seen := map[volumeKey]bool{}
ecCounts := map[volumeKey]*ecFileCounts{}
statsFor := func(c string) *CollectionStatistics {
s, ok := out[c]
if !ok {
s = &CollectionStatistics{Collection: c}
out[c] = s
}
return s
}
for _, dc := range t.DataCenterInfos {
for _, r := range dc.RackInfos {
for _, dn := range r.DataNodeInfos {
for _, diskInfo := range dn.DiskInfos {
for _, vi := range diskInfo.VolumeInfos {
s := statsFor(vi.Collection)
s.PhysicalSize += vi.Size
key := volumeKey{vi.Collection, vi.Id}
if seen[key] {
continue
}
seen[key] = true
s.Size += vi.Size
s.FileCount += vi.FileCount
s.DeleteCount += vi.DeleteCount
s.DeletedByteCount += vi.DeletedByteCount
s.VolumeCount++
}
for _, esi := range diskInfo.EcShardInfos {
s := statsFor(esi.Collection)
s.PhysicalSize += uint64(erasure_coding.EcShardsTotalSize(esi))
s.Size += uint64(erasure_coding.EcShardsDataSize(esi, 0))
key := volumeKey{esi.Collection, esi.Id}
agg, ok := ecCounts[key]
if !ok {
agg = &ecFileCounts{collection: esi.Collection}
ecCounts[key] = agg
s.VolumeCount++
}
if esi.FileCount > agg.fileCount {
agg.fileCount = esi.FileCount
}
agg.deleteCount += esi.DeleteCount
}
}
}
}
}
for _, agg := range ecCounts {
if s := out[agg.collection]; s != nil {
s.FileCount += agg.fileCount
s.DeleteCount += agg.deleteCount
}
}
return out
}
func statsTopology(t *testing.T) *Topology {
t.Helper()
topo := NewTopology("stats", nil, 32*1024*1024*1024, 5, false)
rack := topo.GetOrCreateDataCenter("dc1").GetOrCreateRack("rack1")
nodes := make([]*DataNode, 3)
for i := range nodes {
nodes[i] = rack.GetOrCreateDataNode(
"10.0.0."+string(rune('1'+i)), 8080, 18080, "", "", map[string]uint32{"": 1000, "ssd": 1000})
}
// Two collections, replicated volumes, one volume on a second disk type.
// Replicas agree here; disagreement is covered on its own, where the
// listing this is compared against is itself order dependent.
report := func(dn *DataNode, msgs ...*master_pb.VolumeInformationMessage) {
topo.SyncDataNodeRegistration(msgs, dn)
}
vol := func(id uint32, collection string, size uint64, diskType string) *master_pb.VolumeInformationMessage {
return &master_pb.VolumeInformationMessage{
Id: id, Collection: collection, Size: size, FileCount: uint64(id) * 10,
DeleteCount: uint64(id), DeletedByteCount: uint64(id) * 100,
Version: 3, ReplicaPlacement: 1, DiskType: diskType,
}
}
report(nodes[0], vol(1, "bucket-a", 1000, ""), vol(2, "bucket-a", 2000, ""), vol(5, "bucket-b", 700, "ssd"))
report(nodes[1], vol(1, "bucket-a", 1000, ""), vol(3, "bucket-b", 3000, ""))
report(nodes[2], vol(2, "bucket-a", 2000, ""), vol(3, "bucket-b", 3000, ""))
// Ec shards for a third collection, spread over two nodes, with the file
// count reported differently by each holder.
topo.SyncDataNodeEcShards([]*master_pb.VolumeEcShardInformationMessage{
{Id: 9, Collection: "bucket-c", EcIndexBits: 0x1f, ShardSizes: []int64{10, 20, 30, 40, 50}, FileCount: 40, DeleteCount: 2},
}, nodes[0])
topo.SyncDataNodeEcShards([]*master_pb.VolumeEcShardInformationMessage{
{Id: 9, Collection: "bucket-c", EcIndexBits: 0x3e0, ShardSizes: []int64{60, 70, 80, 90, 100}, FileCount: 44, DeleteCount: 3},
}, nodes[1])
return topo
}
// The summary replaces callers adding up a full topology listing, so it has to
// produce what that produced: these numbers enforce bucket quotas.
func TestCollectionStatisticsMatchesAFullListing(t *testing.T) {
topo := statsTopology(t)
want := referenceCollectionStatistics(topo.ToTopologyInfo(VolumeFilter{}))
got := map[string]*CollectionStatistics{}
for _, s := range topo.CollectionStatistics() {
got[s.Collection] = s
}
if len(got) != len(want) {
t.Fatalf("summarised %d collections, the listing had %d: %v vs %v", len(got), len(want), got, want)
}
for name, expected := range want {
actual, found := got[name]
if !found {
t.Errorf("collection %s is missing from the summary", name)
continue
}
if *actual != *expected {
t.Errorf("collection %s:\n summary %+v\n listing %+v", name, *actual, *expected)
}
}
}
// Replicas disagree while a write is landing or a heartbeat is late. The full
// listing walked the topology in map order and took whichever replica it
// reached first, so its answer was not stable; the summary takes the largest.
func TestCollectionStatisticsPicksTheLargestReplica(t *testing.T) {
topo := NewTopology("stats", nil, 32*1024*1024*1024, 5, false)
rack := topo.GetOrCreateDataCenter("dc1").GetOrCreateRack("rack1")
small := rack.GetOrCreateDataNode("10.0.0.1", 8080, 18080, "", "a", map[string]uint32{"": 100})
large := rack.GetOrCreateDataNode("10.0.0.2", 8080, 18080, "", "b", map[string]uint32{"": 100})
behind := &master_pb.VolumeInformationMessage{
Id: 1, Collection: "bucket-a", Size: 1000, FileCount: 10, Version: 3, ReplicaPlacement: 1,
}
ahead := &master_pb.VolumeInformationMessage{
Id: 1, Collection: "bucket-a", Size: 4000, FileCount: 40, Version: 3, ReplicaPlacement: 1,
}
topo.SyncDataNodeRegistration([]*master_pb.VolumeInformationMessage{behind}, small)
topo.SyncDataNodeRegistration([]*master_pb.VolumeInformationMessage{ahead}, large)
for i := 0; i < 8; i++ {
stats := topo.CollectionStatistics()
if len(stats) != 1 {
t.Fatalf("expected one collection, got %d", len(stats))
}
if stats[0].Size != 4000 || stats[0].FileCount != 40 {
t.Fatalf("run %d reported size %d files %d, want the larger replica's 4000 and 40",
i, stats[0].Size, stats[0].FileCount)
}
if stats[0].PhysicalSize != 5000 {
t.Fatalf("run %d reported physical size %d, want both replicas summed", i, stats[0].PhysicalSize)
}
}
}
func statsByCollection(topo *Topology) map[string]*CollectionStatistics {
out := map[string]*CollectionStatistics{}
for _, s := range topo.CollectionStatistics() {
out[s.Collection] = s
}
return out
}
// The .ecx and .ecj travel with the shards, so a holder still loading them
// reports zero and must not pin the volume's count down.
func TestCollectionStatisticsTakesTheLargestEcFileCount(t *testing.T) {
topo := NewTopology("stats", nil, 32*1024*1024*1024, 5, false)
rack := topo.GetOrCreateDataCenter("dc1").GetOrCreateRack("rack1")
loading := rack.GetOrCreateDataNode("10.0.0.1", 8080, 18080, "", "a", map[string]uint32{"": 100})
loaded := rack.GetOrCreateDataNode("10.0.0.2", 8080, 18080, "", "b", map[string]uint32{"": 100})
topo.SyncDataNodeEcShards([]*master_pb.VolumeEcShardInformationMessage{
{Id: 11, Collection: "bucket-b", EcIndexBits: 0x7f, ShardSizes: []int64{1, 1, 1, 1, 1, 1, 1}, FileCount: 0},
}, loading)
topo.SyncDataNodeEcShards([]*master_pb.VolumeEcShardInformationMessage{
{Id: 11, Collection: "bucket-b", EcIndexBits: 0x3f80, ShardSizes: []int64{1, 1, 1, 1, 1, 1, 1}, FileCount: 6},
}, loaded)
stats := statsByCollection(topo)["bucket-b"]
if stats == nil {
t.Fatal("expected bucket-b to be reported")
}
if stats.FileCount != 6 {
t.Errorf("file count %d, want the largest any holder reported (6)", stats.FileCount)
}
if stats.VolumeCount != 1 {
t.Errorf("volume count %d, want one volume however many holders report shards", stats.VolumeCount)
}
// 14 shards of 1 byte each; only the 10 data shards count as logical.
if stats.PhysicalSize != 14 {
t.Errorf("physical size %d, want every shard counted (14)", stats.PhysicalSize)
}
if stats.Size != 10 {
t.Errorf("size %d, want the data shards only (10)", stats.Size)
}
}
// A collection with both kinds has to have them added together.
func TestCollectionStatisticsCountsRegularAndEcTogether(t *testing.T) {
topo := NewTopology("stats", nil, 32*1024*1024*1024, 5, false)
rack := topo.GetOrCreateDataCenter("dc1").GetOrCreateRack("rack1")
dn := rack.GetOrCreateDataNode("10.0.0.1", 8080, 18080, "", "a", map[string]uint32{"": 100})
topo.SyncDataNodeRegistration([]*master_pb.VolumeInformationMessage{
{Id: 1, Collection: "mixed", Size: 500, FileCount: 5, DeleteCount: 1, DeletedByteCount: 50, Version: 3},
}, dn)
topo.SyncDataNodeEcShards([]*master_pb.VolumeEcShardInformationMessage{
{Id: 2, Collection: "mixed", EcIndexBits: 0x3fff,
ShardSizes: []int64{1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1}, FileCount: 7, DeleteCount: 2},
}, dn)
stats := statsByCollection(topo)["mixed"]
if stats == nil {
t.Fatal("expected the mixed collection to be reported")
}
if stats.VolumeCount != 2 {
t.Errorf("volume count %d, want the regular and the ec volume (2)", stats.VolumeCount)
}
if stats.FileCount != 12 {
t.Errorf("file count %d, want 5 regular plus 7 ec", stats.FileCount)
}
if stats.DeleteCount != 3 {
t.Errorf("delete count %d, want 1 regular plus 2 ec", stats.DeleteCount)
}
if stats.Size != 510 {
t.Errorf("size %d, want 500 regular plus 10 data shards", stats.Size)
}
if stats.PhysicalSize != 514 {
t.Errorf("physical size %d, want 500 regular plus all 14 shards", stats.PhysicalSize)
}
}
// Quotas are enforced on size less deletions, so the replica holding the most
// live data is the one to count. The replica with the biggest raw size can be
// the one that has deleted the most, and counting that one would report a
// bucket smaller than it is and leave it writable over its quota.
func TestCollectionStatisticsPicksTheReplicaHoldingTheMostLiveData(t *testing.T) {
topo := NewTopology("stats", nil, 32*1024*1024*1024, 5, false)
rack := topo.GetOrCreateDataCenter("dc1").GetOrCreateRack("rack1")
bigMostlyDeleted := rack.GetOrCreateDataNode("10.0.0.1", 8080, 18080, "", "a", map[string]uint32{"": 100})
smallerButLive := rack.GetOrCreateDataNode("10.0.0.2", 8080, 18080, "", "b", map[string]uint32{"": 100})
topo.SyncDataNodeRegistration([]*master_pb.VolumeInformationMessage{{
Id: 1, Collection: "bucket-a", Size: 1000, DeletedByteCount: 900,
Version: 3, ReplicaPlacement: 1,
}}, bigMostlyDeleted)
topo.SyncDataNodeRegistration([]*master_pb.VolumeInformationMessage{{
Id: 1, Collection: "bucket-a", Size: 900, DeletedByteCount: 0,
Version: 3, ReplicaPlacement: 1,
}}, smallerButLive)
stats := statsByCollection(topo)["bucket-a"]
if stats == nil {
t.Fatal("expected bucket-a to be reported")
}
live := stats.Size - stats.DeletedByteCount
if live != 900 {
t.Errorf("reported %d bytes live (size %d less %d deleted), want the 900 one replica holds",
live, stats.Size, stats.DeletedByteCount)
}
if stats.PhysicalSize != 1900 {
t.Errorf("physical size %d, want both replicas summed", stats.PhysicalSize)
}
}
// Live usage is read as the collection's size less its deletions, so a volume
// reporting more deleted bytes than it holds must not cancel live bytes
// belonging to other volumes in the same bucket.
func TestCollectionStatisticsDeletionsNeverExceedTheVolume(t *testing.T) {
topo := NewTopology("stats", nil, 32*1024*1024*1024, 5, false)
dn := topo.GetOrCreateDataCenter("dc1").GetOrCreateRack("rack1").
GetOrCreateDataNode("10.0.0.1", 8080, 18080, "", "a", map[string]uint32{"": 100})
topo.SyncDataNodeRegistration([]*master_pb.VolumeInformationMessage{
// More deleted bytes than the volume holds, which a compaction can
// leave behind, and a healthy volume beside it.
{Id: 1, Collection: "bucket-a", Size: 100, DeletedByteCount: 500, Version: 3},
{Id: 2, Collection: "bucket-a", Size: 1000, DeletedByteCount: 0, Version: 3},
}, dn)
stats := statsByCollection(topo)["bucket-a"]
if stats == nil {
t.Fatal("expected bucket-a to be reported")
}
if stats.DeletedByteCount > stats.Size {
t.Errorf("deletions %d exceed size %d, so live usage reads as zero", stats.DeletedByteCount, stats.Size)
}
if live := stats.Size - stats.DeletedByteCount; live != 1000 {
t.Errorf("reported %d bytes live, want the 1000 the second volume holds", live)
}
}