Ctrl K
ComunidadGuías…

bldr/util/cstate/cstate_test.go

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package cstate

import (
	"context"
	"errors"
	"sync"
	"sync/atomic"
	"testing"
)

// TestCState tests the CState type.
func TestCState(t *testing.T) {
	// Start CState and prepare an atomic record of watcher updates.
	ctx := context.Background()
	st := NewCState(0)
	var lastState atomic.Int32
	go func() {
		_ = st.Execute(ctx, nil)
	}()

	// Register a watcher that records the current integer state.
	_, _ = st.AddWatcher(ctx, true, func(ctx context.Context, state int) {
		lastState.Store(int32(state)) //nolint:gosec
	})

	// Apply one state update through the CState operation queue.
	_, err := st.Apply(ctx, func(ctx context.Context, v *CStateWriter[int]) (dirty bool, err error) {
		v.SetObj(1)
		return true, nil
	})
	if err != nil {
		t.Fatal(err.Error())
	}

	// Assert that the watcher observed the applied state.
	if lsl := lastState.Load(); lsl != 1 {
		t.Fatal("Expected lastState to be 1, got", lsl)
	}
}

// TestCStateMultipleOperations tests multiple operations on CState.
func TestCStateMultipleOperations(t *testing.T) {
	// Start CState before queuing multiple state updates.
	ctx := context.Background()
	st := NewCState(0)
	go func() {
		_ = st.Execute(ctx, nil)
	}()

	// Apply five updates and report the first rejected operation.
	for i := 1; i <= 5; i++ {
		_, err := st.Apply(ctx, func(ctx context.Context, v *CStateWriter[int]) (dirty bool, err error) {
			v.SetObj(i)
			return true, nil
		})
		if err != nil {
			t.Fatalf("Error applying operation %d: %v", i, err)
		}
	}

	// Wait until the CState watcher observes the final value.
	err := st.Wait(ctx, func(ctx context.Context, val int) (bool, error) {
		return val == 5, nil
	})
	if err != nil {
		t.Fatal("Error waiting for final state:", err)
	}

	// Read the completed state while CState holds its view lock.
	var finalState int
	err = st.View(ctx, func(ctx context.Context, value int) error {
		finalState = value
		return nil
	})
	if err != nil {
		t.Fatal("Error viewing final state:", err)
	}

	// Assert the view returns the last queued value.
	if finalState != 5 {
		t.Fatalf("Expected final state to be 5, got %d", finalState)
	}
}

// TestCStateWatchers tests adding and removing watchers.
func TestCStateWatchers(t *testing.T) {
	// Start CState before registering its test watchers.
	ctx := context.Background()
	st := NewCState(0)
	go func() {
		_ = st.Execute(ctx, nil)
	}()

	// Count callbacks under a mutex shared by both watchers.
	watcherCalls := make(map[int]int)
	var mu sync.Mutex

	// Register the initial watcher and check registration succeeds.
	remove1, err := st.AddWatcher(ctx, true, func(ctx context.Context, state int) {
		mu.Lock()
		watcherCalls[1]++
		mu.Unlock()
	})
	if err != nil {
		t.Fatal("Error adding watcher 1:", err)
	}

	// Register the second watcher without an initial delivery.
	remove2, err := st.AddWatcher(ctx, false, func(ctx context.Context, state int) {
		mu.Lock()
		watcherCalls[2]++
		mu.Unlock()
	})
	if err != nil {
		t.Fatal("Error adding watcher 2:", err)
	}

	// Apply the first state change to invoke both registered watchers.
	_, err = st.Apply(ctx, func(ctx context.Context, v *CStateWriter[int]) (dirty bool, err error) {
		v.SetObj(1)
		return true, nil
	})
	if err != nil {
		t.Fatal("Error applying state change:", err)
	}

	// Remove both watchers before sending another state change.
	remove1()
	remove2()

	// Apply a second state change after both watchers are removed.
	_, err = st.Apply(ctx, func(ctx context.Context, v *CStateWriter[int]) (dirty bool, err error) {
		v.SetObj(2)
		return true, nil
	})
	if err != nil {
		t.Fatal("Error applying second state change:", err)
	}

	// Execute holds the lock while it calls watchers, so taking it here waits
	// for any watcher calls from the second change.
	err = st.View(ctx, func(ctx context.Context, value int) error {
		return nil
	})
	if err != nil {
		t.Fatal("Error waiting for watcher calls:", err)
	}

	// Verify removal stops later calls without changing earlier counts.
	mu.Lock()
	if watcherCalls[1] != 2 {
		t.Fatalf("Expected watcher 1 to be called 2 times, got %d", watcherCalls[1])
	}
	if watcherCalls[2] != 1 {
		t.Fatalf("Expected watcher 2 to be called 1 time, got %d", watcherCalls[2])
	}
	mu.Unlock()
}

// TestCStateErrorHandling tests error handling in CState operations.
func TestCStateErrorHandling(t *testing.T) {
	// Start CState before checking operation error propagation.
	ctx := context.Background()
	st := NewCState(0)
	go func() {
		_ = st.Execute(ctx, nil)
	}()

	// Queue an operation failure and verify CState returns that error.
	expectedError := errors.New("test error")
	_, err := st.Apply(ctx, func(ctx context.Context, v *CStateWriter[int]) (dirty bool, err error) {
		return false, expectedError
	})
	if err != expectedError {
		t.Fatalf("Expected error %v, got %v", expectedError, err)
	}

	// Prepare a canceled context for the next queued operation.
	canceledCtx, cancel := context.WithCancel(ctx)
	cancel()

	// Verify CState rejects work submitted after context cancellation.
	_, err = st.Apply(canceledCtx, func(ctx context.Context, v *CStateWriter[int]) (dirty bool, err error) {
		return true, nil
	})
	if err != context.Canceled {
		t.Fatalf("Expected context.Canceled error, got %v", err)
	}
}