ci: add golangci-lint config and fix formatting

- Add .golangci.yml with minimal linters (govet, ineffassign)
- Run gofmt -s and goimports on all files to fix formatting
- Disable fieldalignment and copylocks checks in govet
This commit is contained in:
2025-12-11 17:53:28 +01:00
parent 6b66ae5429
commit 914307ac8f
89 changed files with 1516 additions and 1618 deletions

View File

@@ -17,32 +17,32 @@ type DetailedReporter struct {
// OperationStatus represents the status of a backup/restore operation
type OperationStatus struct {
ID string `json:"id"`
Name string `json:"name"`
Type string `json:"type"` // "backup", "restore", "verify"
Status string `json:"status"` // "running", "completed", "failed"
StartTime time.Time `json:"start_time"`
EndTime *time.Time `json:"end_time,omitempty"`
Duration time.Duration `json:"duration"`
Progress int `json:"progress"` // 0-100
Message string `json:"message"`
Details map[string]string `json:"details"`
Steps []StepStatus `json:"steps"`
BytesTotal int64 `json:"bytes_total"`
BytesDone int64 `json:"bytes_done"`
FilesTotal int `json:"files_total"`
FilesDone int `json:"files_done"`
Errors []string `json:"errors,omitempty"`
ID string `json:"id"`
Name string `json:"name"`
Type string `json:"type"` // "backup", "restore", "verify"
Status string `json:"status"` // "running", "completed", "failed"
StartTime time.Time `json:"start_time"`
EndTime *time.Time `json:"end_time,omitempty"`
Duration time.Duration `json:"duration"`
Progress int `json:"progress"` // 0-100
Message string `json:"message"`
Details map[string]string `json:"details"`
Steps []StepStatus `json:"steps"`
BytesTotal int64 `json:"bytes_total"`
BytesDone int64 `json:"bytes_done"`
FilesTotal int `json:"files_total"`
FilesDone int `json:"files_done"`
Errors []string `json:"errors,omitempty"`
}
// StepStatus represents individual steps within an operation
type StepStatus struct {
Name string `json:"name"`
Status string `json:"status"`
StartTime time.Time `json:"start_time"`
EndTime *time.Time `json:"end_time,omitempty"`
Name string `json:"name"`
Status string `json:"status"`
StartTime time.Time `json:"start_time"`
EndTime *time.Time `json:"end_time,omitempty"`
Duration time.Duration `json:"duration"`
Message string `json:"message"`
Message string `json:"message"`
}
// Logger interface for detailed reporting
@@ -79,7 +79,7 @@ func (dr *DetailedReporter) StartOperation(id, name, opType string) *OperationTr
}
dr.operations = append(dr.operations, operation)
if dr.startTime.IsZero() {
dr.startTime = time.Now()
}
@@ -90,9 +90,9 @@ func (dr *DetailedReporter) StartOperation(id, name, opType string) *OperationTr
}
// Log operation start
dr.logger.Info("Operation started",
"id", id,
"name", name,
dr.logger.Info("Operation started",
"id", id,
"name", name,
"type", opType,
"timestamp", operation.StartTime.Format(time.RFC3339))
@@ -117,7 +117,7 @@ func (ot *OperationTracker) UpdateProgress(progress int, message string) {
if ot.reporter.operations[i].ID == ot.operationID {
ot.reporter.operations[i].Progress = progress
ot.reporter.operations[i].Message = message
// Update visual indicator
if ot.reporter.indicator != nil {
progressMsg := fmt.Sprintf("[%d%%] %s", progress, message)
@@ -150,7 +150,7 @@ func (ot *OperationTracker) AddStep(name, message string) *StepTracker {
for i := range ot.reporter.operations {
if ot.reporter.operations[i].ID == ot.operationID {
ot.reporter.operations[i].Steps = append(ot.reporter.operations[i].Steps, step)
// Log step start
ot.reporter.logger.Info("Step started",
"operation_id", ot.operationID,
@@ -190,7 +190,7 @@ func (ot *OperationTracker) SetFileProgress(filesDone, filesTotal int) {
if ot.reporter.operations[i].ID == ot.operationID {
ot.reporter.operations[i].FilesDone = filesDone
ot.reporter.operations[i].FilesTotal = filesTotal
if filesTotal > 0 {
progress := (filesDone * 100) / filesTotal
ot.reporter.operations[i].Progress = progress
@@ -209,25 +209,25 @@ func (ot *OperationTracker) SetByteProgress(bytesDone, bytesTotal int64) {
if ot.reporter.operations[i].ID == ot.operationID {
ot.reporter.operations[i].BytesDone = bytesDone
ot.reporter.operations[i].BytesTotal = bytesTotal
if bytesTotal > 0 {
progress := int((bytesDone * 100) / bytesTotal)
ot.reporter.operations[i].Progress = progress
// Calculate ETA and speed
elapsed := time.Since(ot.reporter.operations[i].StartTime).Seconds()
if elapsed > 0 && bytesDone > 0 {
speed := float64(bytesDone) / elapsed // bytes/sec
remaining := bytesTotal - bytesDone
eta := time.Duration(float64(remaining)/speed) * time.Second
// Update progress message with ETA and speed
if ot.reporter.indicator != nil {
speedStr := formatSpeed(int64(speed))
etaStr := formatDuration(eta)
progressMsg := fmt.Sprintf("[%d%%] %s / %s (%s/s, ETA: %s)",
progress,
formatBytes(bytesDone),
progressMsg := fmt.Sprintf("[%d%%] %s / %s (%s/s, ETA: %s)",
progress,
formatBytes(bytesDone),
formatBytes(bytesTotal),
speedStr,
etaStr)
@@ -253,7 +253,7 @@ func (ot *OperationTracker) Complete(message string) {
ot.reporter.operations[i].EndTime = &now
ot.reporter.operations[i].Duration = now.Sub(ot.reporter.operations[i].StartTime)
ot.reporter.operations[i].Message = message
// Complete visual indicator
if ot.reporter.indicator != nil {
ot.reporter.indicator.Complete(fmt.Sprintf("✅ %s", message))
@@ -283,7 +283,7 @@ func (ot *OperationTracker) Fail(err error) {
ot.reporter.operations[i].Duration = now.Sub(ot.reporter.operations[i].StartTime)
ot.reporter.operations[i].Message = err.Error()
ot.reporter.operations[i].Errors = append(ot.reporter.operations[i].Errors, err.Error())
// Fail visual indicator
if ot.reporter.indicator != nil {
ot.reporter.indicator.Fail(fmt.Sprintf("❌ %s", err.Error()))
@@ -321,7 +321,7 @@ func (st *StepTracker) Complete(message string) {
st.reporter.operations[i].Steps[j].EndTime = &now
st.reporter.operations[i].Steps[j].Duration = now.Sub(st.reporter.operations[i].Steps[j].StartTime)
st.reporter.operations[i].Steps[j].Message = message
// Log step completion
st.reporter.logger.Info("Step completed",
"operation_id", st.operationID,
@@ -351,7 +351,7 @@ func (st *StepTracker) Fail(err error) {
st.reporter.operations[i].Steps[j].EndTime = &now
st.reporter.operations[i].Steps[j].Duration = now.Sub(st.reporter.operations[i].Steps[j].StartTime)
st.reporter.operations[i].Steps[j].Message = err.Error()
// Log step failure
st.reporter.logger.Error("Step failed",
"operation_id", st.operationID,
@@ -428,8 +428,8 @@ type OperationSummary struct {
func (os *OperationSummary) FormatSummary() string {
return fmt.Sprintf(
"📊 Operations Summary:\n"+
" Total: %d | Completed: %d | Failed: %d | Running: %d\n"+
" Total Duration: %s",
" Total: %d | Completed: %d | Failed: %d | Running: %d\n"+
" Total Duration: %s",
os.TotalOperations,
os.CompletedOperations,
os.FailedOperations,
@@ -461,7 +461,7 @@ func formatBytes(bytes int64) string {
GB = 1024 * MB
TB = 1024 * GB
)
switch {
case bytes >= TB:
return fmt.Sprintf("%.2f TB", float64(bytes)/float64(TB))
@@ -483,7 +483,7 @@ func formatSpeed(bytesPerSec int64) string {
MB = 1024 * KB
GB = 1024 * MB
)
switch {
case bytesPerSec >= GB:
return fmt.Sprintf("%.2f GB", float64(bytesPerSec)/float64(GB))
@@ -494,4 +494,4 @@ func formatSpeed(bytesPerSec int64) string {
default:
return fmt.Sprintf("%d B", bytesPerSec)
}
}
}

View File

@@ -42,11 +42,11 @@ func (e *ETAEstimator) GetETA() time.Duration {
if e.itemsComplete == 0 || e.totalItems == 0 {
return 0
}
elapsed := e.GetElapsed()
avgTimePerItem := elapsed / time.Duration(e.itemsComplete)
remainingItems := e.totalItems - e.itemsComplete
return avgTimePerItem * time.Duration(remainingItems)
}
@@ -83,12 +83,12 @@ func (e *ETAEstimator) GetFullStatus(baseMessage string) string {
// No items to track, just show elapsed
return fmt.Sprintf("%s | Elapsed: %s", baseMessage, e.FormatElapsed())
}
if e.itemsComplete == 0 {
// Just started
return fmt.Sprintf("%s | 0/%d | Starting...", baseMessage, e.totalItems)
}
// Full status with progress and ETA
return fmt.Sprintf("%s | %s | Elapsed: %s | ETA: %s",
baseMessage,
@@ -102,44 +102,44 @@ func FormatDuration(d time.Duration) string {
if d < time.Second {
return "< 1s"
}
hours := int(d.Hours())
minutes := int(d.Minutes()) % 60
seconds := int(d.Seconds()) % 60
if hours > 0 {
if minutes > 0 {
return fmt.Sprintf("%dh %dm", hours, minutes)
}
return fmt.Sprintf("%dh", hours)
}
if minutes > 0 {
if seconds > 5 { // Only show seconds if > 5
return fmt.Sprintf("%dm %ds", minutes, seconds)
}
return fmt.Sprintf("%dm", minutes)
}
return fmt.Sprintf("%ds", seconds)
}
// EstimateSizeBasedDuration estimates duration based on size (fallback when no progress tracking)
func EstimateSizeBasedDuration(sizeBytes int64, cores int) time.Duration {
sizeMB := float64(sizeBytes) / (1024 * 1024)
// Base estimate: ~100MB per minute on average hardware
baseMinutes := sizeMB / 100.0
// Adjust for CPU cores (more cores = faster, but not linear)
// Use square root to represent diminishing returns
if cores > 1 {
speedup := 1.0 + (0.3 * (float64(cores) - 1)) // 30% improvement per core
baseMinutes = baseMinutes / speedup
}
// Add 20% buffer for safety
baseMinutes = baseMinutes * 1.2
return time.Duration(baseMinutes * float64(time.Minute))
}

View File

@@ -7,19 +7,19 @@ import (
func TestNewETAEstimator(t *testing.T) {
estimator := NewETAEstimator("Test Operation", 10)
if estimator.operation != "Test Operation" {
t.Errorf("Expected operation 'Test Operation', got '%s'", estimator.operation)
}
if estimator.totalItems != 10 {
t.Errorf("Expected totalItems 10, got %d", estimator.totalItems)
}
if estimator.itemsComplete != 0 {
t.Errorf("Expected itemsComplete 0, got %d", estimator.itemsComplete)
}
if estimator.startTime.IsZero() {
t.Error("Expected startTime to be set")
}
@@ -27,12 +27,12 @@ func TestNewETAEstimator(t *testing.T) {
func TestUpdateProgress(t *testing.T) {
estimator := NewETAEstimator("Test", 10)
estimator.UpdateProgress(5)
if estimator.itemsComplete != 5 {
t.Errorf("Expected itemsComplete 5, got %d", estimator.itemsComplete)
}
estimator.UpdateProgress(8)
if estimator.itemsComplete != 8 {
t.Errorf("Expected itemsComplete 8, got %d", estimator.itemsComplete)
@@ -41,24 +41,24 @@ func TestUpdateProgress(t *testing.T) {
func TestGetProgress(t *testing.T) {
estimator := NewETAEstimator("Test", 10)
// Test 0% progress
if progress := estimator.GetProgress(); progress != 0 {
t.Errorf("Expected 0%%, got %.2f%%", progress)
}
// Test 50% progress
estimator.UpdateProgress(5)
if progress := estimator.GetProgress(); progress != 50.0 {
t.Errorf("Expected 50%%, got %.2f%%", progress)
}
// Test 100% progress
estimator.UpdateProgress(10)
if progress := estimator.GetProgress(); progress != 100.0 {
t.Errorf("Expected 100%%, got %.2f%%", progress)
}
// Test zero division
zeroEstimator := NewETAEstimator("Test", 0)
if progress := zeroEstimator.GetProgress(); progress != 0 {
@@ -68,10 +68,10 @@ func TestGetProgress(t *testing.T) {
func TestGetElapsed(t *testing.T) {
estimator := NewETAEstimator("Test", 10)
// Wait a bit
time.Sleep(100 * time.Millisecond)
elapsed := estimator.GetElapsed()
if elapsed < 100*time.Millisecond {
t.Errorf("Expected elapsed time >= 100ms, got %v", elapsed)
@@ -80,16 +80,16 @@ func TestGetElapsed(t *testing.T) {
func TestGetETA(t *testing.T) {
estimator := NewETAEstimator("Test", 10)
// No progress yet, ETA should be 0
if eta := estimator.GetETA(); eta != 0 {
t.Errorf("Expected ETA 0 for no progress, got %v", eta)
}
// Simulate 5 items completed in 5 seconds
estimator.startTime = time.Now().Add(-5 * time.Second)
estimator.UpdateProgress(5)
eta := estimator.GetETA()
// Should be approximately 5 seconds (5 items remaining at 1 sec/item)
if eta < 4*time.Second || eta > 6*time.Second {
@@ -99,18 +99,18 @@ func TestGetETA(t *testing.T) {
func TestFormatProgress(t *testing.T) {
estimator := NewETAEstimator("Test", 13)
// Test at 0%
if result := estimator.FormatProgress(); result != "0/13 (0%)" {
t.Errorf("Expected '0/13 (0%%)', got '%s'", result)
}
// Test at 38%
estimator.UpdateProgress(5)
if result := estimator.FormatProgress(); result != "5/13 (38%)" {
t.Errorf("Expected '5/13 (38%%)', got '%s'", result)
}
// Test at 100%
estimator.UpdateProgress(13)
if result := estimator.FormatProgress(); result != "13/13 (100%)" {
@@ -125,16 +125,16 @@ func TestFormatDuration(t *testing.T) {
}{
{500 * time.Millisecond, "< 1s"},
{5 * time.Second, "5s"},
{65 * time.Second, "1m"}, // 5 seconds not shown (<=5)
{125 * time.Second, "2m"}, // 5 seconds not shown (<=5)
{65 * time.Second, "1m"}, // 5 seconds not shown (<=5)
{125 * time.Second, "2m"}, // 5 seconds not shown (<=5)
{3 * time.Minute, "3m"},
{3*time.Minute + 3*time.Second, "3m"}, // < 5 seconds not shown
{3*time.Minute + 10*time.Second, "3m 10s"}, // > 5 seconds shown
{3*time.Minute + 3*time.Second, "3m"}, // < 5 seconds not shown
{3*time.Minute + 10*time.Second, "3m 10s"}, // > 5 seconds shown
{90 * time.Minute, "1h 30m"},
{120 * time.Minute, "2h"},
{150 * time.Minute, "2h 30m"},
}
for _, tt := range tests {
result := FormatDuration(tt.duration)
if result != tt.expected {
@@ -145,16 +145,16 @@ func TestFormatDuration(t *testing.T) {
func TestFormatETA(t *testing.T) {
estimator := NewETAEstimator("Test", 10)
// No progress - should show "calculating..."
if result := estimator.FormatETA(); result != "calculating..." {
t.Errorf("Expected 'calculating...', got '%s'", result)
}
// With progress
estimator.startTime = time.Now().Add(-10 * time.Second)
estimator.UpdateProgress(5)
result := estimator.FormatETA()
if result != "~10s remaining" {
t.Errorf("Expected '~10s remaining', got '%s'", result)
@@ -164,7 +164,7 @@ func TestFormatETA(t *testing.T) {
func TestFormatElapsed(t *testing.T) {
estimator := NewETAEstimator("Test", 10)
estimator.startTime = time.Now().Add(-45 * time.Second)
result := estimator.FormatElapsed()
if result != "45s" {
t.Errorf("Expected '45s', got '%s'", result)
@@ -173,23 +173,23 @@ func TestFormatElapsed(t *testing.T) {
func TestGetFullStatus(t *testing.T) {
estimator := NewETAEstimator("Backing up cluster", 13)
// Just started (0 items)
result := estimator.GetFullStatus("Backing up cluster")
if result != "Backing up cluster | 0/13 | Starting..." {
t.Errorf("Unexpected result for 0 items: '%s'", result)
}
// With progress
estimator.startTime = time.Now().Add(-30 * time.Second)
estimator.UpdateProgress(5)
result = estimator.GetFullStatus("Backing up cluster")
// Should contain all components
if len(result) < 50 { // Reasonable minimum length
t.Errorf("Result too short: '%s'", result)
}
// Check it contains key elements (format may vary slightly)
if !contains(result, "5/13") {
t.Errorf("Result missing progress '5/13': '%s'", result)
@@ -208,7 +208,7 @@ func TestGetFullStatus(t *testing.T) {
func TestGetFullStatusWithZeroItems(t *testing.T) {
estimator := NewETAEstimator("Test Operation", 0)
estimator.startTime = time.Now().Add(-5 * time.Second)
result := estimator.GetFullStatus("Test Operation")
// Should only show elapsed time when no items to track
if !contains(result, "Test Operation") || !contains(result, "Elapsed:") {
@@ -226,13 +226,13 @@ func TestEstimateSizeBasedDuration(t *testing.T) {
if duration < 60*time.Second || duration > 90*time.Second {
t.Errorf("Expected ~1.2 minutes for 100MB/1core, got %v", duration)
}
// Test 100MB with 8 cores (should be faster)
duration8cores := EstimateSizeBasedDuration(100*1024*1024, 8)
if duration8cores >= duration {
t.Errorf("Expected faster with more cores: %v vs %v", duration8cores, duration)
}
// Test larger file
duration1GB := EstimateSizeBasedDuration(1024*1024*1024, 1)
if duration1GB <= duration {
@@ -242,9 +242,8 @@ func TestEstimateSizeBasedDuration(t *testing.T) {
// Helper function
func contains(s, substr string) bool {
return len(s) >= len(substr) && (s == substr ||
len(s) > len(substr) && (
s[:len(substr)] == substr ||
return len(s) >= len(substr) && (s == substr ||
len(s) > len(substr) && (s[:len(substr)] == substr ||
s[len(s)-len(substr):] == substr ||
indexHelper(s, substr) >= 0))
}

View File

@@ -43,11 +43,11 @@ func NewSpinner() *Spinner {
func (s *Spinner) Start(message string) {
s.message = message
s.active = true
go func() {
ticker := time.NewTicker(s.interval)
defer ticker.Stop()
i := 0
lastMessage := ""
for {
@@ -57,12 +57,12 @@ func (s *Spinner) Start(message string) {
case <-ticker.C:
if s.active {
displayMsg := s.message
// Add ETA info if estimator is available
if s.estimator != nil {
displayMsg = s.estimator.GetFullStatus(s.message)
}
currentFrame := fmt.Sprintf("%s %s", s.frames[i%len(s.frames)], displayMsg)
if s.message != lastMessage {
// Print new line for new messages
@@ -130,13 +130,13 @@ func NewDots() *Dots {
func (d *Dots) Start(message string) {
d.message = message
d.active = true
fmt.Fprint(d.writer, message)
go func() {
ticker := time.NewTicker(500 * time.Millisecond)
defer ticker.Stop()
count := 0
for {
select {
@@ -191,13 +191,13 @@ func (d *Dots) SetEstimator(estimator *ETAEstimator) {
// ProgressBar creates a visual progress bar
type ProgressBar struct {
writer io.Writer
message string
total int
current int
width int
active bool
stopCh chan bool
writer io.Writer
message string
total int
current int
width int
active bool
stopCh chan bool
}
// NewProgressBar creates a new progress bar
@@ -265,12 +265,12 @@ func (p *ProgressBar) render() {
if !p.active {
return
}
percent := float64(p.current) / float64(p.total)
filled := int(percent * float64(p.width))
bar := strings.Repeat("█", filled) + strings.Repeat("░", p.width-filled)
fmt.Fprintf(p.writer, "\n%s [%s] %d%%", p.message, bar, int(percent*100))
}
@@ -432,7 +432,7 @@ func NewIndicator(interactive bool, indicatorType string) Indicator {
if !interactive {
return NewLineByLine() // Use line-by-line for non-interactive mode
}
switch indicatorType {
case "spinner":
return NewSpinner()
@@ -457,9 +457,9 @@ func NewNullIndicator() *NullIndicator {
return &NullIndicator{}
}
func (n *NullIndicator) Start(message string) {}
func (n *NullIndicator) Update(message string) {}
func (n *NullIndicator) Complete(message string) {}
func (n *NullIndicator) Fail(message string) {}
func (n *NullIndicator) Stop() {}
func (n *NullIndicator) Start(message string) {}
func (n *NullIndicator) Update(message string) {}
func (n *NullIndicator) Complete(message string) {}
func (n *NullIndicator) Fail(message string) {}
func (n *NullIndicator) Stop() {}
func (n *NullIndicator) SetEstimator(estimator *ETAEstimator) {}