Skip to main content
Glama
snapshot_integration_test.go9.66 kB
package process import ( "sync" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // TestProcessSnapshotAtomicConsistency tests that ProcessSnapshot provides atomic access func TestProcessSnapshotAtomicConsistency(b *testing.T) { proc := &Process{ ID: "atomic-test", Name: "atomic", Script: "echo test", Status: StatusRunning, StartTime: time.Now(), EndTime: nil, ExitCode: nil, } const numReaders = 100 const numIterations = 1000 var inconsistencyCount int32 var wg sync.WaitGroup wg.Add(numReaders + 1) // +1 for writer // Start a writer that modifies process state go func() { defer wg.Done() for i := 0; i < numIterations; i++ { proc.mu.Lock() // Simulate state transition switch proc.Status { case StatusRunning: proc.Status = StatusStopped now := time.Now() proc.EndTime = &now code := 0 proc.ExitCode = &code case StatusStopped: proc.Status = StatusFailed code := 1 proc.ExitCode = &code case StatusFailed: proc.Status = StatusRunning proc.EndTime = nil proc.ExitCode = nil } proc.mu.Unlock() time.Sleep(time.Microsecond) } }() // Start readers that check consistency for i := 0; i < numReaders; i++ { go func(readerID int) { defer wg.Done() for j := 0; j < numIterations/10; j++ { snapshot := proc.GetSnapshot() // Check consistency rules if snapshot.Status == StatusRunning { // Running processes should not have EndTime or ExitCode if snapshot.EndTime != nil || snapshot.ExitCode != nil { inconsistencyCount++ b.Errorf("Reader %d: Running process has EndTime=%v or ExitCode=%v", readerID, snapshot.EndTime, snapshot.ExitCode) } } else if snapshot.Status == StatusStopped || snapshot.Status == StatusFailed { // Finished processes should have EndTime and ExitCode if snapshot.EndTime == nil { inconsistencyCount++ b.Errorf("Reader %d: Finished process (%s) missing EndTime", readerID, snapshot.Status) } if snapshot.ExitCode == nil { inconsistencyCount++ b.Errorf("Reader %d: Finished process (%s) missing ExitCode", readerID, snapshot.Status) } } // Verify snapshot convenience methods work correctly isRunning := snapshot.IsRunning() isFinished := snapshot.IsFinished() if isRunning && isFinished { inconsistencyCount++ b.Errorf("Reader %d: Process cannot be both running and finished", readerID) } if !isRunning && !isFinished && snapshot.Status != StatusPending { inconsistencyCount++ b.Errorf("Reader %d: Process must be either running, finished, or pending", readerID) } } }(i) } wg.Wait() if inconsistencyCount > 0 { b.Errorf("Found %d consistency violations", inconsistencyCount) } } // TestProcessSnapshotVsIndividualGetters compares consistency between approaches func TestProcessSnapshotVsIndividualGetters(t *testing.T) { proc := &Process{ ID: "compare-test", Name: "compare", Script: "echo test", Status: StatusRunning, StartTime: time.Now(), EndTime: nil, ExitCode: nil, } const numTests = 1000 var snapshotInconsistencies int var getterInconsistencies int for i := 0; i < numTests; i++ { // Test ProcessSnapshot approach go func() { proc.mu.Lock() // Simulate state transition if proc.Status == StatusRunning { proc.Status = StatusStopped now := time.Now() proc.EndTime = &now code := 0 proc.ExitCode = &code } proc.mu.Unlock() }() // Immediately read with ProcessSnapshot (atomic) snapshot := proc.GetSnapshot() if snapshot.Status == StatusRunning && (snapshot.EndTime != nil || snapshot.ExitCode != nil) { snapshotInconsistencies++ } // Reset for next test proc.mu.Lock() proc.Status = StatusRunning proc.EndTime = nil proc.ExitCode = nil proc.mu.Unlock() // Test individual getters approach (non-atomic) go func() { proc.mu.Lock() // Simulate state transition if proc.Status == StatusRunning { proc.Status = StatusStopped now := time.Now() proc.EndTime = &now code := 0 proc.ExitCode = &code } proc.mu.Unlock() }() // Immediately read with individual getters (potential race) status := proc.GetStatus() endTime := proc.GetEndTime() exitCode := proc.GetExitCode() if status == StatusRunning && (endTime != nil || exitCode != nil) { getterInconsistencies++ } // Reset for next test proc.mu.Lock() proc.Status = StatusRunning proc.EndTime = nil proc.ExitCode = nil proc.mu.Unlock() time.Sleep(time.Microsecond) // Small delay to allow races } t.Logf("Snapshot inconsistencies: %d/%d", snapshotInconsistencies, numTests) t.Logf("Individual getter inconsistencies: %d/%d", getterInconsistencies, numTests) // ProcessSnapshot should have significantly fewer inconsistencies assert.LessOrEqual(t, snapshotInconsistencies, getterInconsistencies, "ProcessSnapshot should have fewer or equal inconsistencies compared to individual getters") } // TestProcessSnapshotMethods tests ProcessSnapshot convenience methods func TestProcessSnapshotMethods(t *testing.T) { testCases := []struct { name string status ProcessStatus endTime *time.Time exitCode *int expectRunning bool expectFinished bool }{ { name: "Running process", status: StatusRunning, endTime: nil, exitCode: nil, expectRunning: true, expectFinished: false, }, { name: "Pending process", status: StatusPending, endTime: nil, exitCode: nil, expectRunning: false, expectFinished: false, }, { name: "Stopped process", status: StatusStopped, endTime: timePtr(time.Now().Add(-2 * time.Minute)), // Ended 2 minutes ago exitCode: intPtr(0), expectRunning: false, expectFinished: true, }, { name: "Failed process", status: StatusFailed, endTime: timePtr(time.Now().Add(-1 * time.Minute)), // Ended 1 minute ago exitCode: intPtr(1), expectRunning: false, expectFinished: true, }, { name: "Success process", status: StatusSuccess, endTime: timePtr(time.Now()), // Just ended exitCode: intPtr(0), expectRunning: false, expectFinished: true, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { baseTime := time.Now().Add(-5 * time.Minute) // Start 5 minutes ago snapshot := ProcessSnapshot{ ID: "test", Name: "test", Script: "echo test", Status: tc.status, StartTime: baseTime, EndTime: tc.endTime, ExitCode: tc.exitCode, } assert.Equal(t, tc.expectRunning, snapshot.IsRunning(), "IsRunning() result") assert.Equal(t, tc.expectFinished, snapshot.IsFinished(), "IsFinished() result") // Test Duration method duration := snapshot.Duration() assert.NotZero(t, duration, "Duration should not be zero") if tc.endTime != nil && !tc.endTime.IsZero() { expectedDuration := tc.endTime.Sub(snapshot.StartTime) assert.Equal(t, expectedDuration, duration, "Duration should match EndTime - StartTime") } else { // For running/pending processes, duration should be positive (time since start) assert.Positive(t, duration, "Duration should be positive for running processes") } }) } } // TestProcessSnapshotStringRepresentation tests String() method func TestProcessSnapshotStringRepresentation(t *testing.T) { snapshot := ProcessSnapshot{ ID: "test-123", Name: "test-process", Script: "echo test", Status: StatusRunning, StartTime: time.Now(), EndTime: nil, ExitCode: nil, } str := snapshot.String() assert.Contains(t, str, "test-123", "String should contain process ID") assert.Contains(t, str, "test-process", "String should contain process name") assert.Contains(t, str, "running", "String should contain status") } // TestProcessSnapshotConcurrentAccess tests concurrent access to ProcessSnapshot func TestProcessSnapshotConcurrentAccess(t *testing.T) { proc := &Process{ ID: "concurrent-snapshot-test", Name: "concurrent-snapshot", Script: "echo test", Status: StatusRunning, StartTime: time.Now(), EndTime: nil, ExitCode: nil, } const numGoroutines = 100 const numIterations = 100 var wg sync.WaitGroup wg.Add(numGoroutines) // All goroutines read concurrently using ProcessSnapshot for i := 0; i < numGoroutines; i++ { go func(id int) { defer wg.Done() for j := 0; j < numIterations; j++ { snapshot := proc.GetSnapshot() // Verify snapshot data is consistent require.NotEmpty(t, snapshot.ID, "Snapshot ID should not be empty") require.NotEmpty(t, snapshot.Name, "Snapshot Name should not be empty") require.NotZero(t, snapshot.StartTime, "Snapshot StartTime should not be zero") // Use snapshot methods _ = snapshot.IsRunning() _ = snapshot.IsFinished() _ = snapshot.Duration() _ = snapshot.String() } }(i) } // Wait for all goroutines to complete done := make(chan bool) go func() { wg.Wait() done <- true }() select { case <-done: // Success case <-time.After(10 * time.Second): t.Fatal("Test timed out - possible deadlock in ProcessSnapshot") } } // Helper function to create int pointer func intPtr(i int) *int { return &i } // Helper function to create time pointer func timePtr(t time.Time) *time.Time { return &t }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/standardbeagle/brummer'

If you have feedback or need assistance with the MCP directory API, please join our Discord server