558 lines
13 KiB
Go
558 lines
13 KiB
Go
package commands
|
|
|
|
import (
|
|
"bytes"
|
|
"io"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
type mockClock struct {
|
|
mu sync.RWMutex
|
|
now time.Time
|
|
}
|
|
|
|
func newMockClock() *mockClock {
|
|
return &mockClock{
|
|
now: time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC),
|
|
}
|
|
}
|
|
|
|
func (c *mockClock) Now() time.Time {
|
|
c.mu.RLock()
|
|
defer c.mu.RUnlock()
|
|
return c.now
|
|
}
|
|
|
|
func (c *mockClock) Advance(d time.Duration) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
c.now = c.now.Add(d)
|
|
}
|
|
|
|
func (c *mockClock) Set(t time.Time) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
c.now = t
|
|
}
|
|
|
|
func captureOutput(fn func()) string {
|
|
old := os.Stdout
|
|
r, w, _ := os.Pipe()
|
|
os.Stdout = w
|
|
|
|
defer func() {
|
|
_ = w.Close()
|
|
os.Stdout = old
|
|
}()
|
|
|
|
fn()
|
|
|
|
var buf bytes.Buffer
|
|
_, _ = io.Copy(&buf, r)
|
|
return buf.String()
|
|
}
|
|
|
|
func TestNewProgressIndicator(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
total int
|
|
description string
|
|
expected *ProgressIndicator
|
|
}{
|
|
{
|
|
name: "basic progress indicator",
|
|
total: 100,
|
|
description: "Test operation",
|
|
expected: &ProgressIndicator{
|
|
total: 100,
|
|
current: 0,
|
|
description: "Test operation",
|
|
showETA: true,
|
|
},
|
|
},
|
|
{
|
|
name: "zero total",
|
|
total: 0,
|
|
description: "Empty operation",
|
|
expected: &ProgressIndicator{
|
|
total: 0,
|
|
current: 0,
|
|
description: "Empty operation",
|
|
showETA: true,
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
pi := NewProgressIndicator(tt.total, tt.description)
|
|
|
|
if pi.total != tt.expected.total {
|
|
t.Errorf("expected total %d, got %d", tt.expected.total, pi.total)
|
|
}
|
|
if pi.current != tt.expected.current {
|
|
t.Errorf("expected current %d, got %d", tt.expected.current, pi.current)
|
|
}
|
|
if pi.description != tt.expected.description {
|
|
t.Errorf("expected description %q, got %q", tt.expected.description, pi.description)
|
|
}
|
|
if pi.showETA != tt.expected.showETA {
|
|
t.Errorf("expected showETA %v, got %v", tt.expected.showETA, pi.showETA)
|
|
}
|
|
if pi.startTime.IsZero() {
|
|
t.Error("expected startTime to be set")
|
|
}
|
|
if pi.lastUpdate.IsZero() {
|
|
t.Error("expected lastUpdate to be set")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestProgressIndicator_Update(t *testing.T) {
|
|
clock := newMockClock()
|
|
pi := newProgressIndicatorWithClock(10, "Test", clock)
|
|
|
|
pi.Update(5)
|
|
if pi.current != 5 {
|
|
t.Errorf("expected current to be 5, got %d", pi.current)
|
|
}
|
|
|
|
originalLastUpdate := pi.lastUpdate
|
|
clock.Advance(50 * time.Millisecond)
|
|
pi.Update(6)
|
|
if pi.current != 6 {
|
|
t.Errorf("expected current to be 6, got %d", pi.current)
|
|
}
|
|
if !pi.lastUpdate.Equal(originalLastUpdate) {
|
|
t.Error("expected lastUpdate to remain unchanged due to throttling")
|
|
}
|
|
|
|
clock.Advance(150 * time.Millisecond)
|
|
lastUpdateBefore := pi.lastUpdate
|
|
pi.Update(7)
|
|
if pi.current != 7 {
|
|
t.Errorf("expected current to be 7, got %d", pi.current)
|
|
}
|
|
if pi.lastUpdate.Equal(lastUpdateBefore) {
|
|
t.Error("expected lastUpdate to be updated after throttling period")
|
|
}
|
|
}
|
|
|
|
func TestProgressIndicator_Increment(t *testing.T) {
|
|
pi := NewProgressIndicator(10, "Test")
|
|
originalCurrent := pi.current
|
|
|
|
pi.Increment()
|
|
if pi.current != originalCurrent+1 {
|
|
t.Errorf("expected current to be %d, got %d", originalCurrent+1, pi.current)
|
|
}
|
|
}
|
|
|
|
func TestProgressIndicator_SetDescription(t *testing.T) {
|
|
pi := NewProgressIndicator(10, "Original")
|
|
newDesc := "New description"
|
|
|
|
pi.SetDescription(newDesc)
|
|
if pi.description != newDesc {
|
|
t.Errorf("expected description %q, got %q", newDesc, pi.description)
|
|
}
|
|
}
|
|
|
|
func TestProgressIndicator_Complete(t *testing.T) {
|
|
pi := NewProgressIndicator(10, "Test")
|
|
pi.current = 5
|
|
|
|
output := captureOutput(func() {
|
|
pi.Complete()
|
|
})
|
|
|
|
if pi.current != pi.total {
|
|
t.Errorf("expected current to be %d, got %d", pi.total, pi.current)
|
|
}
|
|
|
|
if !strings.Contains(output, "Test") {
|
|
t.Error("expected output to contain description")
|
|
}
|
|
if !strings.Contains(output, "10/10") {
|
|
t.Error("expected output to contain final count")
|
|
}
|
|
if !strings.Contains(output, "100.0%") {
|
|
t.Error("expected output to contain 100%")
|
|
}
|
|
}
|
|
|
|
func TestProgressIndicator_display(t *testing.T) {
|
|
pi := NewProgressIndicator(10, "Test")
|
|
pi.current = 3
|
|
|
|
output := captureOutput(func() {
|
|
pi.display()
|
|
})
|
|
|
|
if !strings.Contains(output, "Test") {
|
|
t.Error("expected output to contain description")
|
|
}
|
|
if !strings.Contains(output, "3/10") {
|
|
t.Error("expected output to contain current/total")
|
|
}
|
|
if !strings.Contains(output, "30.0%") {
|
|
t.Error("expected output to contain percentage")
|
|
}
|
|
if !strings.Contains(output, "[") && !strings.Contains(output, "]") {
|
|
t.Error("expected output to contain progress bar")
|
|
}
|
|
}
|
|
|
|
func TestNewSimpleProgressIndicator(t *testing.T) {
|
|
clock := newMockClock()
|
|
spi := newSimpleProgressIndicatorWithClock("Test operation", clock)
|
|
|
|
if spi.description != "Test operation" {
|
|
t.Errorf("expected description %q, got %q", "Test operation", spi.description)
|
|
}
|
|
if spi.current != 0 {
|
|
t.Errorf("expected current 0, got %d", spi.current)
|
|
}
|
|
if spi.startTime.IsZero() {
|
|
t.Error("expected startTime to be set")
|
|
}
|
|
}
|
|
|
|
func TestSimpleProgressIndicator_Update(t *testing.T) {
|
|
clock := newMockClock()
|
|
spi := newSimpleProgressIndicatorWithClock("Test", clock)
|
|
|
|
clock.Advance(2 * time.Second)
|
|
|
|
output := captureOutput(func() {
|
|
spi.Update(5)
|
|
})
|
|
|
|
if spi.current != 5 {
|
|
t.Errorf("expected current 5, got %d", spi.current)
|
|
}
|
|
|
|
if !strings.Contains(output, "Test") {
|
|
t.Error("expected output to contain description")
|
|
}
|
|
if !strings.Contains(output, "5 items processed") {
|
|
t.Error("expected output to contain item count")
|
|
}
|
|
if !strings.Contains(output, "2s") {
|
|
t.Error("expected output to contain elapsed time (2s)")
|
|
}
|
|
}
|
|
|
|
func TestSimpleProgressIndicator_Increment(t *testing.T) {
|
|
clock := newMockClock()
|
|
spi := newSimpleProgressIndicatorWithClock("Test", clock)
|
|
originalCurrent := spi.current
|
|
|
|
spi.Increment()
|
|
if spi.current != originalCurrent+1 {
|
|
t.Errorf("expected current to be %d, got %d", originalCurrent+1, spi.current)
|
|
}
|
|
}
|
|
|
|
func TestSimpleProgressIndicator_Complete(t *testing.T) {
|
|
clock := newMockClock()
|
|
spi := newSimpleProgressIndicatorWithClock("Test", clock)
|
|
spi.current = 5
|
|
|
|
clock.Advance(5 * time.Second)
|
|
|
|
output := captureOutput(func() {
|
|
spi.Complete()
|
|
})
|
|
|
|
if !strings.Contains(output, "Test") {
|
|
t.Error("expected output to contain description")
|
|
}
|
|
if !strings.Contains(output, "Completed 5 items") {
|
|
t.Error("expected output to contain completion message")
|
|
}
|
|
if !strings.Contains(output, "5s") {
|
|
t.Error("expected output to contain elapsed time (5s)")
|
|
}
|
|
}
|
|
|
|
func TestNewSpinner(t *testing.T) {
|
|
spinner := NewSpinner("Loading")
|
|
|
|
if spinner.message != "Loading" {
|
|
t.Errorf("expected message %q, got %q", "Loading", spinner.message)
|
|
}
|
|
if spinner.index != 0 {
|
|
t.Errorf("expected index 0, got %d", spinner.index)
|
|
}
|
|
if len(spinner.chars) != 4 {
|
|
t.Errorf("expected 4 chars, got %d", len(spinner.chars))
|
|
}
|
|
if spinner.startTime.IsZero() {
|
|
t.Error("expected startTime to be set")
|
|
}
|
|
}
|
|
|
|
func TestSpinner_Spin(t *testing.T) {
|
|
spinner := NewSpinner("Loading")
|
|
originalIndex := spinner.index
|
|
|
|
output := captureOutput(func() {
|
|
spinner.Spin()
|
|
})
|
|
|
|
if spinner.index != (originalIndex+1)%len(spinner.chars) {
|
|
t.Errorf("expected index to increment, got %d", spinner.index)
|
|
}
|
|
|
|
if !strings.Contains(output, "Loading") {
|
|
t.Error("expected output to contain message")
|
|
}
|
|
if !strings.Contains(output, spinner.chars[originalIndex]) {
|
|
t.Error("expected output to contain current char")
|
|
}
|
|
}
|
|
|
|
func TestSpinner_Complete(t *testing.T) {
|
|
spinner := NewSpinner("Loading")
|
|
|
|
output := captureOutput(func() {
|
|
spinner.Complete()
|
|
})
|
|
|
|
if !strings.Contains(output, "Loading") {
|
|
t.Error("expected output to contain message")
|
|
}
|
|
if !strings.Contains(output, "✓") {
|
|
t.Error("expected output to contain checkmark")
|
|
}
|
|
}
|
|
|
|
func TestNewProgressTracker(t *testing.T) {
|
|
pt := NewProgressTracker("Processing")
|
|
|
|
if pt.description != "Processing" {
|
|
t.Errorf("expected description %q, got %q", "Processing", pt.description)
|
|
}
|
|
if pt.current != 0 {
|
|
t.Errorf("expected current 0, got %d", pt.current)
|
|
}
|
|
if pt.startTime.IsZero() {
|
|
t.Error("expected startTime to be set")
|
|
}
|
|
if pt.lastUpdate.IsZero() {
|
|
t.Error("expected lastUpdate to be set")
|
|
}
|
|
}
|
|
|
|
func TestProgressTracker_Update(t *testing.T) {
|
|
pt := NewProgressTracker("Processing")
|
|
|
|
pt.Update(5)
|
|
if pt.current != 5 {
|
|
t.Errorf("expected current to be 5, got %d", pt.current)
|
|
}
|
|
|
|
originalLastUpdate := pt.lastUpdate
|
|
pt.Update(6)
|
|
if pt.current != 6 {
|
|
t.Errorf("expected current to be 6, got %d", pt.current)
|
|
}
|
|
if !pt.lastUpdate.Equal(originalLastUpdate) {
|
|
t.Error("expected lastUpdate to remain unchanged due to throttling")
|
|
}
|
|
|
|
time.Sleep(250 * time.Millisecond)
|
|
lastUpdateBefore := pt.lastUpdate
|
|
pt.Update(10)
|
|
if pt.current != 10 {
|
|
t.Errorf("expected current to be 10, got %d", pt.current)
|
|
}
|
|
if pt.lastUpdate.Equal(lastUpdateBefore) {
|
|
t.Error("expected lastUpdate to be updated after throttling period")
|
|
}
|
|
}
|
|
|
|
func TestProgressTracker_Increment(t *testing.T) {
|
|
pt := NewProgressTracker("Processing")
|
|
originalCurrent := pt.current
|
|
|
|
pt.Increment()
|
|
if pt.current != originalCurrent+1 {
|
|
t.Errorf("expected current to be %d, got %d", originalCurrent+1, pt.current)
|
|
}
|
|
}
|
|
|
|
func TestProgressTracker_Complete(t *testing.T) {
|
|
pt := NewProgressTracker("Processing")
|
|
pt.current = 10
|
|
|
|
output := captureOutput(func() {
|
|
pt.Complete()
|
|
})
|
|
|
|
if !strings.Contains(output, "Processing") {
|
|
t.Error("expected output to contain description")
|
|
}
|
|
if !strings.Contains(output, "Completed 10 items") {
|
|
t.Error("expected output to contain completion message")
|
|
}
|
|
if !strings.Contains(output, "items/sec") {
|
|
t.Error("expected output to contain rate information")
|
|
}
|
|
}
|
|
|
|
func TestNewBatchProgressIndicator(t *testing.T) {
|
|
bpi := NewBatchProgressIndicator(5, 10, "Batch processing")
|
|
|
|
if bpi.totalBatches != 5 {
|
|
t.Errorf("expected totalBatches 5, got %d", bpi.totalBatches)
|
|
}
|
|
if bpi.currentBatch != 0 {
|
|
t.Errorf("expected currentBatch 0, got %d", bpi.currentBatch)
|
|
}
|
|
if bpi.batchSize != 10 {
|
|
t.Errorf("expected batchSize 10, got %d", bpi.batchSize)
|
|
}
|
|
if bpi.description != "Batch processing" {
|
|
t.Errorf("expected description %q, got %q", "Batch processing", bpi.description)
|
|
}
|
|
if bpi.startTime.IsZero() {
|
|
t.Error("expected startTime to be set")
|
|
}
|
|
}
|
|
|
|
func TestBatchProgressIndicator_UpdateBatch(t *testing.T) {
|
|
bpi := NewBatchProgressIndicator(5, 10, "Batch processing")
|
|
|
|
output := captureOutput(func() {
|
|
bpi.UpdateBatch(2)
|
|
})
|
|
|
|
if bpi.currentBatch != 2 {
|
|
t.Errorf("expected currentBatch 2, got %d", bpi.currentBatch)
|
|
}
|
|
|
|
if !strings.Contains(output, "Batch processing") {
|
|
t.Error("expected output to contain description")
|
|
}
|
|
if !strings.Contains(output, "Batch 2/5") {
|
|
t.Error("expected output to contain batch progress")
|
|
}
|
|
if !strings.Contains(output, "(20 items)") {
|
|
t.Error("expected output to contain item count")
|
|
}
|
|
}
|
|
|
|
func TestBatchProgressIndicator_Complete(t *testing.T) {
|
|
bpi := NewBatchProgressIndicator(5, 10, "Batch processing")
|
|
|
|
output := captureOutput(func() {
|
|
bpi.Complete()
|
|
})
|
|
|
|
if !strings.Contains(output, "Batch processing") {
|
|
t.Error("expected output to contain description")
|
|
}
|
|
if !strings.Contains(output, "Completed 5 batches") {
|
|
t.Error("expected output to contain completion message")
|
|
}
|
|
if !strings.Contains(output, "(50 items)") {
|
|
t.Error("expected output to contain total items")
|
|
}
|
|
}
|
|
|
|
func TestFormatDuration(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
duration time.Duration
|
|
expected string
|
|
}{
|
|
{
|
|
name: "seconds",
|
|
duration: 30 * time.Second,
|
|
expected: "30s",
|
|
},
|
|
{
|
|
name: "minutes",
|
|
duration: 2*time.Minute + 30*time.Second,
|
|
expected: "2.5m",
|
|
},
|
|
{
|
|
name: "hours",
|
|
duration: 1*time.Hour + 30*time.Minute,
|
|
expected: "1.5h",
|
|
},
|
|
{
|
|
name: "zero duration",
|
|
duration: 0,
|
|
expected: "0s",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := formatDuration(tt.duration)
|
|
if result != tt.expected {
|
|
t.Errorf("expected %q, got %q", tt.expected, result)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestProgressIndicator_Concurrency(t *testing.T) {
|
|
pi := NewProgressIndicator(100, "Concurrent test")
|
|
done := make(chan bool)
|
|
|
|
for i := 0; i < 10; i++ {
|
|
go func() {
|
|
for j := 0; j < 10; j++ {
|
|
pi.Increment()
|
|
time.Sleep(1 * time.Millisecond)
|
|
}
|
|
done <- true
|
|
}()
|
|
}
|
|
|
|
for i := 0; i < 10; i++ {
|
|
<-done
|
|
}
|
|
|
|
if pi.current != 100 {
|
|
t.Errorf("expected current to be exactly 100, got %d", pi.current)
|
|
}
|
|
}
|
|
|
|
func TestProgressIndicator_EdgeCases(t *testing.T) {
|
|
t.Run("zero total constructor", func(t *testing.T) {
|
|
pi := NewProgressIndicator(0, "Zero total")
|
|
if pi.total != 0 {
|
|
t.Errorf("expected total 0, got %d", pi.total)
|
|
}
|
|
if pi.current != 0 {
|
|
t.Errorf("expected current 0, got %d", pi.current)
|
|
}
|
|
})
|
|
|
|
t.Run("negative current", func(t *testing.T) {
|
|
pi := NewProgressIndicator(10, "Negative test")
|
|
pi.current = -1
|
|
if pi.current != -1 {
|
|
t.Errorf("expected current -1, got %d", pi.current)
|
|
}
|
|
})
|
|
|
|
t.Run("current greater than total", func(t *testing.T) {
|
|
pi := NewProgressIndicator(10, "Overflow test")
|
|
pi.current = 15
|
|
if pi.current != 15 {
|
|
t.Errorf("expected current 15, got %d", pi.current)
|
|
}
|
|
})
|
|
}
|