diff --git a/cmd/analyze/heap_test.go b/cmd/analyze/heap_test.go new file mode 100644 index 0000000..75076c8 --- /dev/null +++ b/cmd/analyze/heap_test.go @@ -0,0 +1,153 @@ +package main + +import ( + "container/heap" + "testing" +) + +func TestEntryHeap(t *testing.T) { + t.Run("basic heap operations", func(t *testing.T) { + h := &entryHeap{} + heap.Init(h) + + // Push entries with varying sizes. + heap.Push(h, dirEntry{Name: "medium", Size: 500}) + heap.Push(h, dirEntry{Name: "small", Size: 100}) + heap.Push(h, dirEntry{Name: "large", Size: 1000}) + + if h.Len() != 3 { + t.Errorf("Len() = %d, want 3", h.Len()) + } + + // Min-heap: smallest should come out first. + first := heap.Pop(h).(dirEntry) + if first.Name != "small" || first.Size != 100 { + t.Errorf("first Pop() = %v, want {small, 100}", first) + } + + second := heap.Pop(h).(dirEntry) + if second.Name != "medium" || second.Size != 500 { + t.Errorf("second Pop() = %v, want {medium, 500}", second) + } + + third := heap.Pop(h).(dirEntry) + if third.Name != "large" || third.Size != 1000 { + t.Errorf("third Pop() = %v, want {large, 1000}", third) + } + + if h.Len() != 0 { + t.Errorf("Len() after all pops = %d, want 0", h.Len()) + } + }) + + t.Run("empty heap", func(t *testing.T) { + h := &entryHeap{} + heap.Init(h) + + if h.Len() != 0 { + t.Errorf("empty heap Len() = %d, want 0", h.Len()) + } + }) + + t.Run("single element", func(t *testing.T) { + h := &entryHeap{} + heap.Init(h) + + heap.Push(h, dirEntry{Name: "only", Size: 42}) + popped := heap.Pop(h).(dirEntry) + + if popped.Name != "only" || popped.Size != 42 { + t.Errorf("Pop() = %v, want {only, 42}", popped) + } + }) + + t.Run("equal sizes maintain stability", func(t *testing.T) { + h := &entryHeap{} + heap.Init(h) + + heap.Push(h, dirEntry{Name: "a", Size: 100}) + heap.Push(h, dirEntry{Name: "b", Size: 100}) + heap.Push(h, dirEntry{Name: "c", Size: 100}) + + // All have same size, heap property still holds. + for i := 0; i < 3; i++ { + popped := heap.Pop(h).(dirEntry) + if popped.Size != 100 { + t.Errorf("Pop() size = %d, want 100", popped.Size) + } + } + }) +} + +func TestLargeFileHeap(t *testing.T) { + t.Run("basic heap operations", func(t *testing.T) { + h := &largeFileHeap{} + heap.Init(h) + + // Push entries with varying sizes. + heap.Push(h, fileEntry{Name: "medium.bin", Size: 500}) + heap.Push(h, fileEntry{Name: "small.txt", Size: 100}) + heap.Push(h, fileEntry{Name: "large.iso", Size: 1000}) + + if h.Len() != 3 { + t.Errorf("Len() = %d, want 3", h.Len()) + } + + // Min-heap: smallest should come out first. + first := heap.Pop(h).(fileEntry) + if first.Name != "small.txt" || first.Size != 100 { + t.Errorf("first Pop() = %v, want {small.txt, 100}", first) + } + + second := heap.Pop(h).(fileEntry) + if second.Name != "medium.bin" || second.Size != 500 { + t.Errorf("second Pop() = %v, want {medium.bin, 500}", second) + } + + third := heap.Pop(h).(fileEntry) + if third.Name != "large.iso" || third.Size != 1000 { + t.Errorf("third Pop() = %v, want {large.iso, 1000}", third) + } + }) + + t.Run("top N largest pattern", func(t *testing.T) { + // This is how the heap is used in practice: keep top N largest. + h := &largeFileHeap{} + heap.Init(h) + maxSize := 3 + + files := []fileEntry{ + {Name: "a", Size: 50}, + {Name: "b", Size: 200}, + {Name: "c", Size: 30}, + {Name: "d", Size: 150}, + {Name: "e", Size: 300}, + } + + for _, f := range files { + heap.Push(h, f) + if h.Len() > maxSize { + heap.Pop(h) // Remove smallest to keep only top N. + } + } + + if h.Len() != maxSize { + t.Errorf("Len() = %d, want %d", h.Len(), maxSize) + } + + // Extract remaining (should be 3 largest: 150, 200, 300). + var sizes []int64 + for h.Len() > 0 { + sizes = append(sizes, heap.Pop(h).(fileEntry).Size) + } + + // Min-heap pops in ascending order. + want := []int64{150, 200, 300} + for i, s := range sizes { + if s != want[i] { + t.Errorf("sizes[%d] = %d, want %d", i, s, want[i]) + } + } + }) +} + diff --git a/cmd/status/view_test.go b/cmd/status/view_test.go new file mode 100644 index 0000000..6e9254d --- /dev/null +++ b/cmd/status/view_test.go @@ -0,0 +1,114 @@ +package main + +import "testing" + +func TestFormatRate(t *testing.T) { + tests := []struct { + name string + input float64 + want string + }{ + // Below threshold (< 0.01). + {"zero", 0, "0 MB/s"}, + {"tiny", 0.001, "0 MB/s"}, + {"just under threshold", 0.009, "0 MB/s"}, + + // Small rates (0.01 to < 1) — 2 decimal places. + {"at threshold", 0.01, "0.01 MB/s"}, + {"small rate", 0.5, "0.50 MB/s"}, + {"just under 1", 0.99, "0.99 MB/s"}, + + // Medium rates (1 to < 10) — 1 decimal place. + {"exactly 1", 1.0, "1.0 MB/s"}, + {"medium rate", 5.5, "5.5 MB/s"}, + {"just under 10", 9.9, "9.9 MB/s"}, + + // Large rates (>= 10) — no decimal places. + {"exactly 10", 10.0, "10 MB/s"}, + {"large rate", 100.5, "100 MB/s"}, + {"very large", 1000.0, "1000 MB/s"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := formatRate(tt.input) + if got != tt.want { + t.Errorf("formatRate(%v) = %q, want %q", tt.input, got, tt.want) + } + }) + } +} + +func TestShorten(t *testing.T) { + tests := []struct { + name string + input string + maxLen int + want string + }{ + // No truncation needed. + {"empty string", "", 10, ""}, + {"shorter than max", "hello", 10, "hello"}, + {"exactly at max", "hello", 5, "hello"}, + + // Truncation needed. + {"one over max", "hello!", 5, "hell…"}, + {"much longer", "hello world", 5, "hell…"}, + + // Edge cases. + {"maxLen 1", "hello", 1, "…"}, + {"maxLen 2", "hello", 2, "h…"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := shorten(tt.input, tt.maxLen) + if got != tt.want { + t.Errorf("shorten(%q, %d) = %q, want %q", tt.input, tt.maxLen, got, tt.want) + } + }) + } +} + +func TestHumanBytesShort(t *testing.T) { + tests := []struct { + name string + input uint64 + want string + }{ + // Zero and small values. + {"zero", 0, "0"}, + {"one byte", 1, "1"}, + {"999 bytes", 999, "999"}, + + // Kilobyte boundaries. + {"exactly 1KB", 1 << 10, "1K"}, + {"just under 1KB", (1 << 10) - 1, "1023"}, + {"1.5KB rounds to 2K", 1536, "2K"}, + {"999KB", 999 << 10, "999K"}, + + // Megabyte boundaries. + {"exactly 1MB", 1 << 20, "1M"}, + {"just under 1MB", (1 << 20) - 1, "1024K"}, + {"500MB", 500 << 20, "500M"}, + + // Gigabyte boundaries. + {"exactly 1GB", 1 << 30, "1G"}, + {"just under 1GB", (1 << 30) - 1, "1024M"}, + {"100GB", 100 << 30, "100G"}, + + // Terabyte boundaries. + {"exactly 1TB", 1 << 40, "1T"}, + {"just under 1TB", (1 << 40) - 1, "1024G"}, + {"2TB", 2 << 40, "2T"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := humanBytesShort(tt.input) + if got != tt.want { + t.Errorf("humanBytesShort(%d) = %q, want %q", tt.input, got, tt.want) + } + }) + } +}