393 lines
8.5 KiB
Go
393 lines
8.5 KiB
Go
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)
|
|
}
|
|
})
|
|
}
|