package database import ( "context" "database/sql" "errors" "sync/atomic" "testing" "time" ) func TestActorRun(t *testing.T) { t.Run("successful function execution", func(t *testing.T) { actor := &Actor{ DB: nil, // We don't need a real DB for this test ActionChan: make(chan Func, 1), } ctx, cancel := context.WithCancel(context.Background()) defer cancel() // Start the actor errChan := make(chan error, 1) go func() { errChan <- actor.Run(ctx) }() // Send a function to execute var executed atomic.Bool actor.ActionChan <- func(ctx context.Context, db *sql.DB) error { executed.Store(true) return nil } // Give it time to execute time.Sleep(50 * time.Millisecond) if !executed.Load() { t.Error("expected function to be executed") } // Cancel context to stop actor cancel() // Wait for actor to finish if err := <-errChan; err != context.Canceled { t.Errorf("expected context.Canceled, got %v", err) } }) t.Run("function returns error", func(t *testing.T) { actor := &Actor{ DB: nil, ActionChan: make(chan Func, 1), } ctx, cancel := context.WithCancel(context.Background()) defer cancel() // Start the actor errChan := make(chan error, 1) go func() { errChan <- actor.Run(ctx) }() // Send a function that returns an error expectedErr := errors.New("test error") actor.ActionChan <- func(ctx context.Context, db *sql.DB) error { return expectedErr } // Wait for actor to finish with error err := <-errChan if err != expectedErr { t.Errorf("expected error %v, got %v", expectedErr, err) } }) t.Run("multiple functions executed sequentially", func(t *testing.T) { actor := &Actor{ DB: nil, ActionChan: make(chan Func, 10), } ctx, cancel := context.WithCancel(context.Background()) defer cancel() // Start the actor errChan := make(chan error, 1) go func() { errChan <- actor.Run(ctx) }() // Track execution count var count atomic.Int32 // Send multiple functions for i := 0; i < 5; i++ { actor.ActionChan <- func(ctx context.Context, db *sql.DB) error { count.Add(1) return nil } } // Give time for all to execute time.Sleep(100 * time.Millisecond) if count.Load() != 5 { t.Errorf("expected 5 executions, got %d", count.Load()) } // Cancel to stop cancel() err := <-errChan if err != context.Canceled { t.Errorf("expected context.Canceled, got %v", err) } }) t.Run("context cancellation stops actor", func(t *testing.T) { actor := &Actor{ DB: nil, ActionChan: make(chan Func, 1), } ctx, cancel := context.WithCancel(context.Background()) // Start the actor errChan := make(chan error, 1) go func() { errChan <- actor.Run(ctx) }() // Cancel immediately cancel() // Should return context.Canceled err := <-errChan if err != context.Canceled { t.Errorf("expected context.Canceled, got %v", err) } }) t.Run("context deadline exceeded", func(t *testing.T) { actor := &Actor{ DB: nil, ActionChan: make(chan Func, 1), } ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond) defer cancel() // Start the actor errChan := make(chan error, 1) go func() { errChan <- actor.Run(ctx) }() // Wait for timeout err := <-errChan if err != context.DeadlineExceeded { t.Errorf("expected context.DeadlineExceeded, got %v", err) } }) t.Run("panic on nil context", func(t *testing.T) { actor := &Actor{ DB: nil, ActionChan: make(chan Func, 1), } defer func() { if r := recover(); r == nil { t.Fatal("expected panic for nil context") } }() actor.Run(nil) }) t.Run("actor processes db parameter", func(t *testing.T) { // Create a fake DB pointer (we won't use it, just verify it's passed through) fakeDB := &sql.DB{} actor := &Actor{ DB: fakeDB, ActionChan: make(chan Func, 1), } ctx, cancel := context.WithCancel(context.Background()) defer cancel() errChan := make(chan error, 1) go func() { errChan <- actor.Run(ctx) }() type result struct { db *sql.DB } resultChan := make(chan result, 1) actor.ActionChan <- func(ctx context.Context, db *sql.DB) error { resultChan <- result{db: db} return nil } res := <-resultChan if res.db != fakeDB { t.Error("expected function to receive the actor's DB") } cancel() <-errChan }) } func TestWithinTransaction(t *testing.T) { t.Run("wraps function correctly", func(t *testing.T) { innerFunc := func(ctx context.Context, db *sql.DB) error { return nil } wrappedFunc := WithinTransaction(innerFunc) // Verify wrappedFunc is not nil if wrappedFunc == nil { t.Fatal("expected non-nil wrapped function") } // We can't actually call it without a real DB that supports transactions, // but we can verify the type is correct var _ Func = wrappedFunc }) t.Run("returns a Func type", func(t *testing.T) { innerFunc := func(ctx context.Context, db *sql.DB) error { return nil } wrappedFunc := WithinTransaction(innerFunc) // Type assertion to verify it returns Func if _, ok := interface{}(wrappedFunc).(Func); !ok { t.Error("expected WithinTransaction to return Func type") } }) t.Run("preserves error from inner function", func(t *testing.T) { expectedErr := errors.New("inner function error") innerFunc := func(ctx context.Context, db *sql.DB) error { return expectedErr } wrappedFunc := WithinTransaction(innerFunc) // We can verify the function signature is preserved if wrappedFunc == nil { t.Fatal("expected non-nil wrapped function") } }) } func TestActorIntegration(t *testing.T) { t.Run("multiple actors can run concurrently", func(t *testing.T) { actor1 := &Actor{ DB: nil, ActionChan: make(chan Func, 1), } actor2 := &Actor{ DB: nil, ActionChan: make(chan Func, 1), } ctx, cancel := context.WithCancel(context.Background()) defer cancel() errChan1 := make(chan error, 1) errChan2 := make(chan error, 1) go func() { errChan1 <- actor1.Run(ctx) }() go func() { errChan2 <- actor2.Run(ctx) }() var count1, count2 atomic.Int32 // Send work to both actors actor1.ActionChan <- func(ctx context.Context, db *sql.DB) error { count1.Add(1) return nil } actor2.ActionChan <- func(ctx context.Context, db *sql.DB) error { count2.Add(1) return nil } time.Sleep(50 * time.Millisecond) if count1.Load() != 1 { t.Errorf("expected actor1 to execute 1 function, got %d", count1.Load()) } if count2.Load() != 1 { t.Errorf("expected actor2 to execute 1 function, got %d", count2.Load()) } cancel() <-errChan1 <-errChan2 }) t.Run("actor stops on first error", func(t *testing.T) { actor := &Actor{ DB: nil, ActionChan: make(chan Func, 10), } ctx, cancel := context.WithCancel(context.Background()) defer cancel() errChan := make(chan error, 1) go func() { errChan <- actor.Run(ctx) }() var count atomic.Int32 expectedErr := errors.New("stop error") // Queue multiple functions actor.ActionChan <- func(ctx context.Context, db *sql.DB) error { count.Add(1) return nil } actor.ActionChan <- func(ctx context.Context, db *sql.DB) error { count.Add(1) return expectedErr // This should stop the actor } actor.ActionChan <- func(ctx context.Context, db *sql.DB) error { count.Add(1) return nil // Should not be executed } // Wait for error err := <-errChan if err != expectedErr { t.Errorf("expected error %v, got %v", expectedErr, err) } // Should have executed 2 functions (stopped on the error) if count.Load() != 2 { t.Errorf("expected 2 executions before stop, got %d", count.Load()) } }) } func TestFuncType(t *testing.T) { t.Run("Func type signature", func(t *testing.T) { // Verify Func type can be used var f Func = func(ctx context.Context, db *sql.DB) error { return nil } if f == nil { t.Error("expected non-nil Func") } }) t.Run("Func with error", func(t *testing.T) { testErr := errors.New("test error") var f Func = func(ctx context.Context, db *sql.DB) error { return testErr } err := f(context.Background(), nil) if err != testErr { t.Errorf("expected error %v, got %v", testErr, err) } }) t.Run("Func with nil error", func(t *testing.T) { var f Func = func(ctx context.Context, db *sql.DB) error { return nil } err := f(context.Background(), nil) if err != nil { t.Errorf("expected nil error, got %v", err) } }) }