kevo/pkg/replication/state_test.go
Jeremy Tregunna 01cd007e51 feat: Extend WAL to support observers & replication protocol
- WAL package now can notify observers when it writes entries
- WAL can retrieve entries by sequence number
- WAL implements file retention management
- Add replication protocol defined using protobufs
- Implemented compression support for zstd and snappy
- State machine for replication added
- Batch management for streaming from the WAL
2025-04-29 15:03:03 -06:00

162 lines
4.6 KiB
Go

package replication
import (
"errors"
"testing"
"time"
)
func TestStateTracker(t *testing.T) {
// Create a new state tracker
tracker := NewStateTracker()
// Test initial state
if tracker.GetState() != StateConnecting {
t.Errorf("Expected initial state to be StateConnecting, got %s", tracker.GetState())
}
// Test valid state transition
err := tracker.SetState(StateStreamingEntries)
if err != nil {
t.Errorf("Unexpected error for valid transition: %v", err)
}
if tracker.GetState() != StateStreamingEntries {
t.Errorf("Expected state to be StateStreamingEntries, got %s", tracker.GetState())
}
// Test invalid state transition
err = tracker.SetState(StateAcknowledging)
if err == nil {
t.Errorf("Expected error for invalid transition, got nil")
}
if !errors.Is(err, ErrInvalidStateTransition) {
t.Errorf("Expected ErrInvalidStateTransition, got %v", err)
}
if tracker.GetState() != StateStreamingEntries {
t.Errorf("State should not change after invalid transition, got %s", tracker.GetState())
}
// Test complete valid path
validPath := []ReplicaState{
StateApplyingEntries,
StateFsyncPending,
StateAcknowledging,
StateWaitingForData,
StateStreamingEntries,
StateApplyingEntries,
StateFsyncPending,
StateAcknowledging,
StateStreamingEntries,
}
for i, state := range validPath {
err := tracker.SetState(state)
if err != nil {
t.Errorf("Unexpected error at step %d: %v", i, err)
}
if tracker.GetState() != state {
t.Errorf("Expected state to be %s at step %d, got %s", state, i, tracker.GetState())
}
}
// Test error state transition
err = tracker.SetError(errors.New("test error"))
if err != nil {
t.Errorf("Unexpected error setting error state: %v", err)
}
if tracker.GetState() != StateError {
t.Errorf("Expected state to be StateError, got %s", tracker.GetState())
}
if tracker.GetError() == nil {
t.Errorf("Expected error to be set, got nil")
}
if tracker.GetError().Error() != "test error" {
t.Errorf("Expected error message 'test error', got '%s'", tracker.GetError().Error())
}
// Test recovery from error
err = tracker.SetState(StateConnecting)
if err != nil {
t.Errorf("Unexpected error recovering from error state: %v", err)
}
if tracker.GetState() != StateConnecting {
t.Errorf("Expected state to be StateConnecting after recovery, got %s", tracker.GetState())
}
// Test transitions tracking
transitions := tracker.GetTransitions()
// Count the actual transitions we made
transitionCount := len(validPath) + 1 // +1 for error state
if len(transitions) < transitionCount {
t.Errorf("Expected at least %d transitions, got %d", transitionCount, len(transitions))
}
// Test reset
tracker.ResetState()
if tracker.GetState() != StateConnecting {
t.Errorf("Expected state to be StateConnecting after reset, got %s", tracker.GetState())
}
if tracker.GetError() != nil {
t.Errorf("Expected error to be nil after reset, got %v", tracker.GetError())
}
if len(tracker.GetTransitions()) != 0 {
t.Errorf("Expected 0 transitions after reset, got %d", len(tracker.GetTransitions()))
}
}
func TestStateDuration(t *testing.T) {
// Create a new state tracker
tracker := NewStateTracker()
// Initial state duration should be small
initialDuration := tracker.GetStateDuration()
if initialDuration > 100*time.Millisecond {
t.Errorf("Initial state duration too large: %v", initialDuration)
}
// Wait a bit
time.Sleep(200 * time.Millisecond)
// Duration should have increased
afterWaitDuration := tracker.GetStateDuration()
if afterWaitDuration < 200*time.Millisecond {
t.Errorf("Duration did not increase as expected: %v", afterWaitDuration)
}
// Transition to a new state
err := tracker.SetState(StateStreamingEntries)
if err != nil {
t.Fatalf("Unexpected error transitioning states: %v", err)
}
// New state duration should be small again
newStateDuration := tracker.GetStateDuration()
if newStateDuration > 100*time.Millisecond {
t.Errorf("New state duration too large: %v", newStateDuration)
}
}
func TestStateStringRepresentation(t *testing.T) {
testCases := []struct {
state ReplicaState
expected string
}{
{StateConnecting, "CONNECTING"},
{StateStreamingEntries, "STREAMING_ENTRIES"},
{StateApplyingEntries, "APPLYING_ENTRIES"},
{StateFsyncPending, "FSYNC_PENDING"},
{StateAcknowledging, "ACKNOWLEDGING"},
{StateWaitingForData, "WAITING_FOR_DATA"},
{StateError, "ERROR"},
{ReplicaState(999), "UNKNOWN(999)"},
}
for _, tc := range testCases {
t.Run(tc.expected, func(t *testing.T) {
if tc.state.String() != tc.expected {
t.Errorf("Expected state string %s, got %s", tc.expected, tc.state.String())
}
})
}
}