mirror of
https://github.com/turbot/steampipe.git
synced 2025-12-19 18:12:43 -05:00
383 lines
13 KiB
Go
383 lines
13 KiB
Go
package initialisation
|
|
|
|
import (
|
|
"context"
|
|
"runtime"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/spf13/viper"
|
|
pconstants "github.com/turbot/pipe-fittings/v2/constants"
|
|
"github.com/turbot/steampipe/v2/pkg/constants"
|
|
)
|
|
|
|
// TestInitData_ResourceLeakOnPipesMetadataError tests if telemetry is leaked
|
|
// when getPipesMetadata fails after telemetry is initialized
|
|
func TestInitData_ResourceLeakOnPipesMetadataError(t *testing.T) {
|
|
// Setup: Configure a scenario that will cause getPipesMetadata to fail
|
|
// (database name without token)
|
|
originalWorkspaceDB := viper.GetString(pconstants.ArgWorkspaceDatabase)
|
|
originalToken := viper.GetString(pconstants.ArgPipesToken)
|
|
defer func() {
|
|
viper.Set(pconstants.ArgWorkspaceDatabase, originalWorkspaceDB)
|
|
viper.Set(pconstants.ArgPipesToken, originalToken)
|
|
}()
|
|
|
|
viper.Set(pconstants.ArgWorkspaceDatabase, "some-database-name")
|
|
viper.Set(pconstants.ArgPipesToken, "") // Missing token will cause error
|
|
|
|
ctx := context.Background()
|
|
initData := NewInitData()
|
|
|
|
// Run initialization - should fail during getPipesMetadata
|
|
initData.Init(ctx, constants.InvokerQuery)
|
|
|
|
// Verify that an error occurred
|
|
if initData.Result.Error == nil {
|
|
t.Fatal("Expected error from missing cloud token, got nil")
|
|
}
|
|
|
|
// BUG CHECK: Is telemetry cleaned up?
|
|
// If Init() fails after telemetry is initialized but before completion,
|
|
// the telemetry goroutines may be leaked since Cleanup() is not called automatically
|
|
if initData.ShutdownTelemetry != nil {
|
|
t.Logf("WARNING: ShutdownTelemetry function exists but was not called - potential resource leak!")
|
|
t.Logf("BUG FOUND: When Init() fails partway through, telemetry is not automatically cleaned up")
|
|
t.Logf("The caller must remember to call Cleanup() even on error, but this is not enforced")
|
|
|
|
// Clean up manually to prevent leak in test
|
|
initData.Cleanup(ctx)
|
|
}
|
|
}
|
|
|
|
// TestInitData_ResourceLeakOnClientError tests if telemetry is leaked
|
|
// when GetDbClient fails after telemetry is initialized
|
|
func TestInitData_ResourceLeakOnClientError(t *testing.T) {
|
|
// Setup: Configure an invalid connection string
|
|
originalConnString := viper.GetString(pconstants.ArgConnectionString)
|
|
originalWorkspaceDB := viper.GetString(pconstants.ArgWorkspaceDatabase)
|
|
defer func() {
|
|
viper.Set(pconstants.ArgConnectionString, originalConnString)
|
|
viper.Set(pconstants.ArgWorkspaceDatabase, originalWorkspaceDB)
|
|
}()
|
|
|
|
// Set invalid connection string that will fail
|
|
viper.Set(pconstants.ArgConnectionString, "postgresql://invalid:invalid@nonexistent:5432/db")
|
|
viper.Set(pconstants.ArgWorkspaceDatabase, "local")
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
|
defer cancel()
|
|
|
|
initData := NewInitData()
|
|
|
|
// Run initialization - should fail during GetDbClient
|
|
initData.Init(ctx, constants.InvokerQuery)
|
|
|
|
// Verify that an error occurred (either connection error or context timeout)
|
|
if initData.Result.Error == nil {
|
|
t.Fatal("Expected error from invalid connection, got nil")
|
|
}
|
|
|
|
// BUG CHECK: Is telemetry cleaned up?
|
|
if initData.ShutdownTelemetry != nil {
|
|
t.Logf("BUG FOUND: Telemetry initialized but not cleaned up after client connection failure")
|
|
t.Logf("Resource leak: telemetry goroutines may be running indefinitely")
|
|
|
|
// Manual cleanup
|
|
initData.Cleanup(ctx)
|
|
}
|
|
}
|
|
|
|
// TestInitData_CleanupIdempotency tests if calling Cleanup multiple times is safe
|
|
func TestInitData_CleanupIdempotency(t *testing.T) {
|
|
ctx := context.Background()
|
|
initData := NewInitData()
|
|
|
|
// Cleanup on uninitialized data should not panic
|
|
initData.Cleanup(ctx)
|
|
initData.Cleanup(ctx) // Second call should also be safe
|
|
|
|
// Now initialize and cleanup multiple times
|
|
originalWorkspaceDB := viper.GetString(pconstants.ArgWorkspaceDatabase)
|
|
defer func() {
|
|
viper.Set(pconstants.ArgWorkspaceDatabase, originalWorkspaceDB)
|
|
}()
|
|
viper.Set(pconstants.ArgWorkspaceDatabase, "local")
|
|
|
|
// Note: We can't easily test with real initialization here as it requires
|
|
// database setup, but we can test the nil safety of Cleanup
|
|
}
|
|
|
|
// TestInitData_NilExporter tests registering nil exporters
|
|
func TestInitData_NilExporter(t *testing.T) {
|
|
// t.Skip("Demonstrates bug #4750 - HIGH nil pointer panic when registering nil exporter. Remove this skip in bug fix PR commit 1, then fix in commit 2.")
|
|
initData := NewInitData()
|
|
|
|
// Register nil exporter - should this panic or handle gracefully?
|
|
result := initData.RegisterExporters(nil)
|
|
|
|
if result.Result.Error != nil {
|
|
t.Logf("Registering nil exporter returned error: %v", result.Result.Error)
|
|
} else {
|
|
t.Logf("Registering nil exporter succeeded - this might cause issues later")
|
|
}
|
|
}
|
|
|
|
// TestInitData_PartialInitialization tests the state after partial initialization
|
|
func TestInitData_PartialInitialization(t *testing.T) {
|
|
// Setup to fail at getPipesMetadata stage
|
|
originalWorkspaceDB := viper.GetString(pconstants.ArgWorkspaceDatabase)
|
|
originalToken := viper.GetString(pconstants.ArgPipesToken)
|
|
defer func() {
|
|
viper.Set(pconstants.ArgWorkspaceDatabase, originalWorkspaceDB)
|
|
viper.Set(pconstants.ArgPipesToken, originalToken)
|
|
}()
|
|
|
|
viper.Set(pconstants.ArgWorkspaceDatabase, "test-db")
|
|
viper.Set(pconstants.ArgPipesToken, "") // Will fail
|
|
|
|
ctx := context.Background()
|
|
initData := NewInitData()
|
|
|
|
initData.Init(ctx, constants.InvokerQuery)
|
|
|
|
// After failed init, check what state we're in
|
|
if initData.Result.Error == nil {
|
|
t.Fatal("Expected error, got nil")
|
|
}
|
|
|
|
// BUG CHECK: What's partially initialized?
|
|
partiallyInitialized := []string{}
|
|
if initData.ShutdownTelemetry != nil {
|
|
partiallyInitialized = append(partiallyInitialized, "telemetry")
|
|
}
|
|
if initData.Client != nil {
|
|
partiallyInitialized = append(partiallyInitialized, "client")
|
|
}
|
|
if initData.PipesMetadata != nil {
|
|
partiallyInitialized = append(partiallyInitialized, "pipes_metadata")
|
|
}
|
|
|
|
if len(partiallyInitialized) > 0 {
|
|
t.Logf("BUG: Partial initialization detected. Initialized: %v", partiallyInitialized)
|
|
t.Logf("These resources need cleanup but Cleanup() may not be called by users on error")
|
|
|
|
// Cleanup to prevent leak
|
|
initData.Cleanup(ctx)
|
|
}
|
|
}
|
|
|
|
// TestInitData_GoroutineLeak tests for goroutine leaks during failed initialization
|
|
func TestInitData_GoroutineLeak(t *testing.T) {
|
|
// Allow some variance in goroutine count due to runtime behavior
|
|
const goroutineThreshold = 5
|
|
|
|
// Setup to fail
|
|
originalWorkspaceDB := viper.GetString(pconstants.ArgWorkspaceDatabase)
|
|
originalToken := viper.GetString(pconstants.ArgPipesToken)
|
|
defer func() {
|
|
viper.Set(pconstants.ArgWorkspaceDatabase, originalWorkspaceDB)
|
|
viper.Set(pconstants.ArgPipesToken, originalToken)
|
|
}()
|
|
|
|
viper.Set(pconstants.ArgWorkspaceDatabase, "test-db")
|
|
viper.Set(pconstants.ArgPipesToken, "")
|
|
|
|
// Force garbage collection and get baseline
|
|
runtime.GC()
|
|
time.Sleep(100 * time.Millisecond)
|
|
before := runtime.NumGoroutine()
|
|
|
|
ctx := context.Background()
|
|
initData := NewInitData()
|
|
initData.Init(ctx, constants.InvokerQuery)
|
|
|
|
// Don't call Cleanup - simulating user forgetting to cleanup on error
|
|
|
|
// Force garbage collection
|
|
runtime.GC()
|
|
time.Sleep(100 * time.Millisecond)
|
|
after := runtime.NumGoroutine()
|
|
|
|
leaked := after - before
|
|
if leaked > goroutineThreshold {
|
|
t.Logf("BUG FOUND: Potential goroutine leak detected")
|
|
t.Logf("Goroutines before: %d, after: %d, leaked: %d", before, after, leaked)
|
|
t.Logf("When Init() fails, cleanup is not automatic - resources may leak")
|
|
|
|
// Now cleanup and verify goroutines decrease
|
|
initData.Cleanup(ctx)
|
|
runtime.GC()
|
|
time.Sleep(100 * time.Millisecond)
|
|
afterCleanup := runtime.NumGoroutine()
|
|
t.Logf("After manual cleanup: %d goroutines (difference: %d)", afterCleanup, afterCleanup-before)
|
|
} else {
|
|
t.Logf("Goroutine count stable: before=%d, after=%d, diff=%d", before, after, leaked)
|
|
}
|
|
}
|
|
|
|
// TestNewErrorInitData tests the error constructor
|
|
func TestNewErrorInitData(t *testing.T) {
|
|
testErr := context.Canceled
|
|
initData := NewErrorInitData(testErr)
|
|
|
|
if initData == nil {
|
|
t.Fatal("NewErrorInitData returned nil")
|
|
}
|
|
|
|
if initData.Result == nil {
|
|
t.Fatal("Result is nil")
|
|
}
|
|
|
|
if initData.Result.Error != testErr {
|
|
t.Errorf("Expected error %v, got %v", testErr, initData.Result.Error)
|
|
}
|
|
|
|
// BUG CHECK: Can we call Cleanup on error init data?
|
|
ctx := context.Background()
|
|
initData.Cleanup(ctx) // Should not panic
|
|
}
|
|
|
|
// TestInitData_ContextCancellation tests behavior when context is cancelled during init
|
|
func TestInitData_ContextCancellation(t *testing.T) {
|
|
originalWorkspaceDB := viper.GetString(pconstants.ArgWorkspaceDatabase)
|
|
defer func() {
|
|
viper.Set(pconstants.ArgWorkspaceDatabase, originalWorkspaceDB)
|
|
}()
|
|
viper.Set(pconstants.ArgWorkspaceDatabase, "local")
|
|
|
|
// Create a context that's already cancelled
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel() // Cancel immediately
|
|
|
|
initData := NewInitData()
|
|
initData.Init(ctx, constants.InvokerQuery)
|
|
|
|
// Should get context cancellation error
|
|
if initData.Result.Error == nil {
|
|
t.Log("Expected context cancellation error, got nil")
|
|
} else if initData.Result.Error == context.Canceled {
|
|
t.Log("Correctly returned context cancellation error")
|
|
} else {
|
|
t.Logf("Got error: %v (expected context.Canceled)", initData.Result.Error)
|
|
}
|
|
|
|
// BUG CHECK: Are resources cleaned up?
|
|
if initData.ShutdownTelemetry != nil {
|
|
t.Log("BUG: Telemetry initialized even though context was cancelled")
|
|
initData.Cleanup(context.Background())
|
|
}
|
|
}
|
|
|
|
// TestInitData_PanicRecovery tests that panics during init are caught
|
|
func TestInitData_PanicRecovery(t *testing.T) {
|
|
// We can't easily inject a panic into the real init flow without mocking,
|
|
// but we can verify the defer/recover is in place by code inspection
|
|
|
|
// This test documents expected behavior:
|
|
t.Log("Init() has defer/recover to catch panics and convert to errors")
|
|
t.Log("This is good - panics won't crash the application")
|
|
}
|
|
|
|
// TestInitData_DoubleInit tests calling Init twice on same InitData
|
|
func TestInitData_DoubleInit(t *testing.T) {
|
|
originalWorkspaceDB := viper.GetString(pconstants.ArgWorkspaceDatabase)
|
|
originalToken := viper.GetString(pconstants.ArgPipesToken)
|
|
defer func() {
|
|
viper.Set(pconstants.ArgWorkspaceDatabase, originalWorkspaceDB)
|
|
viper.Set(pconstants.ArgPipesToken, originalToken)
|
|
}()
|
|
|
|
// Setup to fail quickly
|
|
viper.Set(pconstants.ArgWorkspaceDatabase, "test-db")
|
|
viper.Set(pconstants.ArgPipesToken, "")
|
|
|
|
ctx := context.Background()
|
|
initData := NewInitData()
|
|
|
|
// First init - will fail
|
|
initData.Init(ctx, constants.InvokerQuery)
|
|
firstErr := initData.Result.Error
|
|
|
|
// Second init on same object - what happens?
|
|
initData.Init(ctx, constants.InvokerQuery)
|
|
secondErr := initData.Result.Error
|
|
|
|
t.Logf("First init error: %v", firstErr)
|
|
t.Logf("Second init error: %v", secondErr)
|
|
|
|
// BUG CHECK: Are there multiple telemetry instances now?
|
|
// Are old resources cleaned up before reinitializing?
|
|
t.Log("WARNING: Calling Init() twice on same InitData may leak resources")
|
|
t.Log("The old ShutdownTelemetry function is overwritten without being called")
|
|
|
|
// Cleanup
|
|
if initData.ShutdownTelemetry != nil {
|
|
initData.Cleanup(ctx)
|
|
}
|
|
}
|
|
|
|
// TestGetDbClient_WithConnectionString tests the client creation with connection string
|
|
func TestGetDbClient_WithConnectionString(t *testing.T) {
|
|
// t.Skip("Demonstrates bug #4767 - GetDbClient returns non-nil client even when error occurs, causing nil pointer panic on Close. Remove this skip in bug fix PR commit 1, then fix in commit 2.")
|
|
originalConnString := viper.GetString(pconstants.ArgConnectionString)
|
|
defer func() {
|
|
viper.Set(pconstants.ArgConnectionString, originalConnString)
|
|
}()
|
|
|
|
// Set an invalid connection string
|
|
viper.Set(pconstants.ArgConnectionString, "postgresql://invalid:invalid@nonexistent:5432/db")
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
|
defer cancel()
|
|
|
|
client, errAndWarnings := GetDbClient(ctx, constants.InvokerQuery)
|
|
|
|
// Should get an error
|
|
if errAndWarnings.Error == nil {
|
|
t.Log("Expected connection error, got nil")
|
|
if client != nil {
|
|
// Clean up if somehow succeeded
|
|
client.Close(ctx)
|
|
}
|
|
} else {
|
|
t.Logf("Got expected error: %v", errAndWarnings.Error)
|
|
}
|
|
|
|
// BUG CHECK: Is client nil when error occurs?
|
|
if errAndWarnings.Error != nil && client != nil {
|
|
t.Log("BUG: Client is not nil even though error occurred")
|
|
t.Log("Caller might try to use the client, leading to undefined behavior")
|
|
client.Close(ctx)
|
|
}
|
|
}
|
|
|
|
// TestGetDbClient_WithoutConnectionString tests the local client creation
|
|
func TestGetDbClient_WithoutConnectionString(t *testing.T) {
|
|
originalConnString := viper.GetString(pconstants.ArgConnectionString)
|
|
defer func() {
|
|
viper.Set(pconstants.ArgConnectionString, originalConnString)
|
|
}()
|
|
|
|
// Clear connection string to force local client
|
|
viper.Set(pconstants.ArgConnectionString, "")
|
|
|
|
// Note: This test will try to start a local database which may not be available
|
|
// in CI environment. We'll use a short timeout.
|
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
|
defer cancel()
|
|
|
|
client, errAndWarnings := GetDbClient(ctx, constants.InvokerQuery)
|
|
|
|
if errAndWarnings.Error != nil {
|
|
t.Logf("Local client creation failed (expected in test environment): %v", errAndWarnings.Error)
|
|
} else {
|
|
t.Log("Local client created successfully")
|
|
if client != nil {
|
|
client.Close(ctx)
|
|
}
|
|
}
|
|
|
|
// The test itself validates that the function doesn't panic
|
|
}
|