Files
x/database/database_test.go
Colin Henry 54aae5f242
All checks were successful
Go / build (1.23) (push) Successful in 3m51s
big updates: tests, bug fixed, documentation. oh my
2026-01-03 15:53:50 -08:00

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)
}
})
}