Files
steampipe/pkg/statushooks/statushooks_test.go
2025-11-11 18:01:56 +08:00

365 lines
9.8 KiB
Go

package statushooks
import (
"context"
"fmt"
"runtime"
"sync"
"testing"
"time"
)
// TestSpinnerCancelChannelNeverInitialized tests that the cancel channel is never initialized
// BUG: The cancel channel field exists but is never initialized or used - it's dead code
func TestSpinnerCancelChannelNeverInitialized(t *testing.T) {
spinner := NewStatusSpinnerHook()
if spinner.cancel != nil {
t.Error("BUG: Cancel channel should be nil (it's never initialized)")
}
// Even after showing and hiding, cancel is never used
spinner.Show()
spinner.Hide()
// The cancel field exists but serves no purpose - this is dead code
t.Log("CONFIRMED: Cancel channel field exists but is completely unused (dead code)")
}
// TestSpinnerConcurrentShowHide tests concurrent Show/Hide calls for race conditions
// BUG: This exposes a race condition on the 'visible' field
func TestSpinnerConcurrentShowHide(t *testing.T) {
t.Skip("Demonstrates bugs #4743, #4744 - Race condition in concurrent Show/Hide. Remove this skip in bug fix PR commit 1, then fix in commit 2.")
spinner := NewStatusSpinnerHook()
var wg sync.WaitGroup
iterations := 100
// Run with: go test -race
for i := 0; i < iterations; i++ {
wg.Add(2)
go func() {
defer wg.Done()
spinner.Show() // BUG: Race on 'visible' field
}()
go func() {
defer wg.Done()
spinner.Hide() // BUG: Race on 'visible' field
}()
}
wg.Wait()
t.Log("Test completed - check for race detector warnings")
}
// TestSpinnerConcurrentUpdate tests concurrent message updates for race conditions
// BUG: This exposes a race condition on spinner.Suffix field
func TestSpinnerConcurrentUpdate(t *testing.T) {
// t.Skip("Demonstrates bugs #4743, #4744 - Race condition in concurrent Update. Remove this skip in bug fix PR commit 1, then fix in commit 2.")
spinner := NewStatusSpinnerHook()
spinner.Show()
defer spinner.Hide()
var wg sync.WaitGroup
iterations := 100
// Run with: go test -race
for i := 0; i < iterations; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
spinner.UpdateSpinnerMessage(fmt.Sprintf("msg-%d", n)) // BUG: Race on spinner.Suffix
}(i)
}
wg.Wait()
t.Log("Test completed - check for race detector warnings")
}
// TestSpinnerMessageDeferredRestart tests that Message() can restart a hidden spinner
// BUG: This exposes a bug where deferred Start() can restart a hidden spinner
func TestSpinnerMessageDeferredRestart(t *testing.T) {
spinner := NewStatusSpinnerHook()
spinner.UpdateSpinnerMessage("test message")
spinner.Show()
// Start a goroutine that will call Hide() while Message() is executing
done := make(chan struct{})
go func() {
time.Sleep(10 * time.Millisecond)
spinner.Hide()
close(done)
}()
// Message() stops the spinner and defers Start()
spinner.Message("test output")
<-done
time.Sleep(50 * time.Millisecond)
// BUG: Spinner might be restarted even though Hide() was called
if spinner.spinner.Active() {
t.Error("BUG FOUND: Spinner was restarted after Hide() due to deferred Start() in Message()")
}
}
// TestSpinnerWarnDeferredRestart tests that Warn() can restart a hidden spinner
// BUG: Similar to Message(), Warn() has the same deferred restart bug
func TestSpinnerWarnDeferredRestart(t *testing.T) {
spinner := NewStatusSpinnerHook()
spinner.UpdateSpinnerMessage("test message")
spinner.Show()
// Start a goroutine that will call Hide() while Warn() is executing
done := make(chan struct{})
go func() {
time.Sleep(10 * time.Millisecond)
spinner.Hide()
close(done)
}()
// Warn() stops the spinner and defers Start()
spinner.Warn("test warning")
<-done
time.Sleep(50 * time.Millisecond)
// BUG: Spinner might be restarted even though Hide() was called
if spinner.spinner.Active() {
t.Error("BUG FOUND: Spinner was restarted after Hide() due to deferred Start() in Warn()")
}
}
// TestSpinnerConcurrentMessageAndHide tests concurrent Message/Warn and Hide calls
// BUG: This exposes race conditions and the deferred restart bug
func TestSpinnerConcurrentMessageAndHide(t *testing.T) {
t.Skip("Demonstrates bugs #4743, #4744 - Race condition in concurrent Message and Hide. Remove this skip in bug fix PR commit 1, then fix in commit 2.")
spinner := NewStatusSpinnerHook()
spinner.UpdateSpinnerMessage("initial message")
spinner.Show()
var wg sync.WaitGroup
iterations := 50
// Run with: go test -race
for i := 0; i < iterations; i++ {
wg.Add(3)
go func(n int) {
defer wg.Done()
spinner.Message(fmt.Sprintf("message-%d", n))
}(i)
go func(n int) {
defer wg.Done()
spinner.Warn(fmt.Sprintf("warning-%d", n))
}(i)
go func() {
defer wg.Done()
if i%10 == 0 {
spinner.Hide()
} else {
spinner.Show()
}
}()
}
wg.Wait()
t.Log("Test completed - check for race detector warnings and restart bugs")
}
// TestProgressReporterConcurrentUpdates tests concurrent updates to progress reporter
// This should be safe due to mutex, but we verify no races occur
func TestProgressReporterConcurrentUpdates(t *testing.T) {
ctx := context.Background()
ctx = AddStatusHooksToContext(ctx, NewStatusSpinnerHook())
reporter := NewSnapshotProgressReporter("test-snapshot")
var wg sync.WaitGroup
iterations := 100
// Run with: go test -race
for i := 0; i < iterations; i++ {
wg.Add(2)
go func(n int) {
defer wg.Done()
reporter.UpdateRowCount(ctx, n)
}(i)
go func(n int) {
defer wg.Done()
reporter.UpdateErrorCount(ctx, 1)
}(i)
}
wg.Wait()
t.Logf("Final counts: rows=%d, errors=%d", reporter.rows, reporter.errors)
}
// TestSpinnerGoroutineLeak tests for goroutine leaks in spinner lifecycle
func TestSpinnerGoroutineLeak(t *testing.T) {
// Allow some warm-up
runtime.GC()
time.Sleep(100 * time.Millisecond)
initialGoroutines := runtime.NumGoroutine()
// Create and destroy many spinners
for i := 0; i < 100; i++ {
spinner := NewStatusSpinnerHook()
spinner.UpdateSpinnerMessage("test message")
spinner.Show()
time.Sleep(1 * time.Millisecond)
spinner.Hide()
}
// Allow cleanup
runtime.GC()
time.Sleep(200 * time.Millisecond)
finalGoroutines := runtime.NumGoroutine()
// Allow some tolerance (5 goroutines)
if finalGoroutines > initialGoroutines+5 {
t.Errorf("Possible goroutine leak: started with %d, ended with %d goroutines",
initialGoroutines, finalGoroutines)
}
}
// TestSpinnerUpdateAfterHide tests updating spinner message after Hide()
func TestSpinnerUpdateAfterHide(t *testing.T) {
spinner := NewStatusSpinnerHook()
spinner.Show()
spinner.UpdateSpinnerMessage("initial message")
spinner.Hide()
// Update after hide - should not start spinner
spinner.UpdateSpinnerMessage("updated message")
if spinner.spinner.Active() {
t.Error("Spinner should not be active after Hide() even if message is updated")
}
}
// TestSpinnerSetStatusRace tests concurrent SetStatus calls
func TestSpinnerSetStatusRace(t *testing.T) {
// t.Skip("Demonstrates bugs #4743, #4744 - Race condition in SetStatus. Remove this skip in bug fix PR commit 1, then fix in commit 2.")
spinner := NewStatusSpinnerHook()
spinner.Show()
var wg sync.WaitGroup
iterations := 100
// Run with: go test -race
for i := 0; i < iterations; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
spinner.SetStatus(fmt.Sprintf("status-%d", n))
}(i)
}
wg.Wait()
spinner.Hide()
}
// TestContextFunctionsNilContext tests that context helper functions handle nil context
func TestContextFunctionsNilContext(t *testing.T) {
// These should not panic with nil context
hooks := StatusHooksFromContext(nil)
if hooks != NullHooks {
t.Error("Expected NullHooks for nil context")
}
progress := SnapshotProgressFromContext(nil)
if progress != NullProgress {
t.Error("Expected NullProgress for nil context")
}
renderer := MessageRendererFromContext(nil)
if renderer == nil {
t.Error("Expected non-nil renderer for nil context")
}
}
// TestSnapshotProgressHelperFunctions tests the helper functions for snapshot progress
func TestSnapshotProgressHelperFunctions(t *testing.T) {
ctx := context.Background()
reporter := NewSnapshotProgressReporter("test")
ctx = AddSnapshotProgressToContext(ctx, reporter)
// These should not panic
UpdateSnapshotProgress(ctx, 10)
SnapshotError(ctx)
if reporter.rows != 10 {
t.Errorf("Expected 10 rows, got %d", reporter.rows)
}
if reporter.errors != 1 {
t.Errorf("Expected 1 error, got %d", reporter.errors)
}
}
// TestSpinnerShowWithoutMessage tests showing spinner without setting a message first
func TestSpinnerShowWithoutMessage(t *testing.T) {
spinner := NewStatusSpinnerHook()
// Show without message - spinner should not start
spinner.Show()
if spinner.spinner.Active() {
t.Error("Spinner should not be active when shown without a message")
}
}
// TestSpinnerMultipleStartStopCycles tests multiple start/stop cycles
func TestSpinnerMultipleStartStopCycles(t *testing.T) {
spinner := NewStatusSpinnerHook()
spinner.UpdateSpinnerMessage("test message")
for i := 0; i < 100; i++ {
spinner.Show()
time.Sleep(1 * time.Millisecond)
spinner.Hide()
}
// Should not crash or leak resources
t.Log("Multiple start/stop cycles completed successfully")
}
// TestSpinnerConcurrentSetStatusAndHide tests race between SetStatus and Hide
func TestSpinnerConcurrentSetStatusAndHide(t *testing.T) {
// t.Skip("Demonstrates bugs #4743, #4744 - Race condition in concurrent SetStatus and Hide. Remove this skip in bug fix PR commit 1, then fix in commit 2.")
spinner := NewStatusSpinnerHook()
spinner.Show()
var wg sync.WaitGroup
done := make(chan struct{})
// Continuously set status
wg.Add(1)
go func() {
defer wg.Done()
for {
select {
case <-done:
return
default:
spinner.SetStatus("updating status")
}
}
}()
// Continuously hide/show
wg.Add(1)
go func() {
defer wg.Done()
for i := 0; i < 50; i++ {
spinner.Hide()
spinner.Show()
}
}()
time.Sleep(100 * time.Millisecond)
close(done)
wg.Wait()
}