mirror of https://github.com/jetkvm/kvm.git
393 lines
9.7 KiB
Go
393 lines
9.7 KiB
Go
//go:build integration && cgo
|
|
// +build integration,cgo
|
|
|
|
package audio
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"os/exec"
|
|
"sync"
|
|
"syscall"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// TestSupervisorRestart tests various supervisor restart scenarios
|
|
func TestSupervisorRestart(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
testFunc func(t *testing.T)
|
|
description string
|
|
}{
|
|
{
|
|
name: "BasicRestart",
|
|
testFunc: testBasicSupervisorRestart,
|
|
description: "Test basic supervisor restart functionality",
|
|
},
|
|
{
|
|
name: "ProcessCrashRestart",
|
|
testFunc: testProcessCrashRestart,
|
|
description: "Test supervisor restart after process crash",
|
|
},
|
|
{
|
|
name: "MaxRestartAttempts",
|
|
testFunc: testMaxRestartAttempts,
|
|
description: "Test supervisor respects max restart attempts",
|
|
},
|
|
{
|
|
name: "ExponentialBackoff",
|
|
testFunc: testExponentialBackoff,
|
|
description: "Test supervisor exponential backoff behavior",
|
|
},
|
|
{
|
|
name: "HealthMonitoring",
|
|
testFunc: testHealthMonitoring,
|
|
description: "Test supervisor health monitoring",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Logf("Running supervisor test: %s - %s", tt.name, tt.description)
|
|
tt.testFunc(t)
|
|
})
|
|
}
|
|
}
|
|
|
|
// testBasicSupervisorRestart tests basic restart functionality
|
|
func testBasicSupervisorRestart(t *testing.T) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
|
defer cancel()
|
|
|
|
// Create a mock supervisor with a simple test command
|
|
supervisor := &AudioInputSupervisor{
|
|
logger: getTestLogger(),
|
|
maxRestarts: 3,
|
|
restartDelay: 100 * time.Millisecond,
|
|
healthCheckInterval: 200 * time.Millisecond,
|
|
}
|
|
|
|
// Use a simple command that will exit quickly for testing
|
|
testCmd := exec.CommandContext(ctx, "sleep", "0.5")
|
|
supervisor.cmd = testCmd
|
|
|
|
var wg sync.WaitGroup
|
|
wg.Add(1)
|
|
|
|
// Start supervisor
|
|
go func() {
|
|
defer wg.Done()
|
|
supervisor.Start(ctx)
|
|
}()
|
|
|
|
// Wait for initial process to start and exit
|
|
time.Sleep(1 * time.Second)
|
|
|
|
// Verify that supervisor attempted restart
|
|
assert.True(t, supervisor.GetRestartCount() > 0, "Supervisor should have attempted restart")
|
|
|
|
// Stop supervisor
|
|
cancel()
|
|
wg.Wait()
|
|
}
|
|
|
|
// testProcessCrashRestart tests restart after process crash
|
|
func testProcessCrashRestart(t *testing.T) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
|
|
defer cancel()
|
|
|
|
supervisor := &AudioInputSupervisor{
|
|
logger: getTestLogger(),
|
|
maxRestarts: 2,
|
|
restartDelay: 200 * time.Millisecond,
|
|
healthCheckInterval: 100 * time.Millisecond,
|
|
}
|
|
|
|
// Create a command that will crash (exit with non-zero code)
|
|
testCmd := exec.CommandContext(ctx, "sh", "-c", "sleep 0.2 && exit 1")
|
|
supervisor.cmd = testCmd
|
|
|
|
var wg sync.WaitGroup
|
|
wg.Add(1)
|
|
|
|
go func() {
|
|
defer wg.Done()
|
|
supervisor.Start(ctx)
|
|
}()
|
|
|
|
// Wait for process to crash and restart attempts
|
|
time.Sleep(2 * time.Second)
|
|
|
|
// Verify restart attempts were made
|
|
restartCount := supervisor.GetRestartCount()
|
|
assert.True(t, restartCount > 0, "Supervisor should have attempted restart after crash")
|
|
assert.True(t, restartCount <= 2, "Supervisor should not exceed max restart attempts")
|
|
|
|
cancel()
|
|
wg.Wait()
|
|
}
|
|
|
|
// testMaxRestartAttempts tests that supervisor respects max restart limit
|
|
func testMaxRestartAttempts(t *testing.T) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
|
|
maxRestarts := 3
|
|
supervisor := &AudioInputSupervisor{
|
|
logger: getTestLogger(),
|
|
maxRestarts: maxRestarts,
|
|
restartDelay: 50 * time.Millisecond,
|
|
healthCheckInterval: 50 * time.Millisecond,
|
|
}
|
|
|
|
// Command that immediately fails
|
|
testCmd := exec.CommandContext(ctx, "false") // 'false' command always exits with code 1
|
|
supervisor.cmd = testCmd
|
|
|
|
var wg sync.WaitGroup
|
|
wg.Add(1)
|
|
|
|
go func() {
|
|
defer wg.Done()
|
|
supervisor.Start(ctx)
|
|
}()
|
|
|
|
// Wait for all restart attempts to complete
|
|
time.Sleep(2 * time.Second)
|
|
|
|
// Verify that supervisor stopped after max attempts
|
|
restartCount := supervisor.GetRestartCount()
|
|
assert.Equal(t, maxRestarts, restartCount, "Supervisor should stop after max restart attempts")
|
|
assert.False(t, supervisor.IsRunning(), "Supervisor should not be running after max attempts")
|
|
|
|
cancel()
|
|
wg.Wait()
|
|
}
|
|
|
|
// testExponentialBackoff tests the exponential backoff behavior
|
|
func testExponentialBackoff(t *testing.T) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
|
|
defer cancel()
|
|
|
|
supervisor := &AudioInputSupervisor{
|
|
logger: getTestLogger(),
|
|
maxRestarts: 3,
|
|
restartDelay: 100 * time.Millisecond, // Base delay
|
|
healthCheckInterval: 50 * time.Millisecond,
|
|
}
|
|
|
|
// Command that fails immediately
|
|
testCmd := exec.CommandContext(ctx, "false")
|
|
supervisor.cmd = testCmd
|
|
|
|
var restartTimes []time.Time
|
|
var mu sync.Mutex
|
|
|
|
// Hook into restart events to measure timing
|
|
originalRestart := supervisor.restart
|
|
supervisor.restart = func() {
|
|
mu.Lock()
|
|
restartTimes = append(restartTimes, time.Now())
|
|
mu.Unlock()
|
|
if originalRestart != nil {
|
|
originalRestart()
|
|
}
|
|
}
|
|
|
|
var wg sync.WaitGroup
|
|
wg.Add(1)
|
|
|
|
go func() {
|
|
defer wg.Done()
|
|
supervisor.Start(ctx)
|
|
}()
|
|
|
|
// Wait for restart attempts
|
|
time.Sleep(3 * time.Second)
|
|
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
|
|
// Verify exponential backoff (each delay should be longer than the previous)
|
|
if len(restartTimes) >= 2 {
|
|
for i := 1; i < len(restartTimes); i++ {
|
|
delay := restartTimes[i].Sub(restartTimes[i-1])
|
|
expectedMinDelay := time.Duration(i) * 100 * time.Millisecond
|
|
assert.True(t, delay >= expectedMinDelay,
|
|
"Restart delay should increase exponentially: attempt %d delay %v should be >= %v",
|
|
i, delay, expectedMinDelay)
|
|
}
|
|
}
|
|
|
|
cancel()
|
|
wg.Wait()
|
|
}
|
|
|
|
// testHealthMonitoring tests the health monitoring functionality
|
|
func testHealthMonitoring(t *testing.T) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
|
|
supervisor := &AudioInputSupervisor{
|
|
logger: getTestLogger(),
|
|
maxRestarts: 2,
|
|
restartDelay: 100 * time.Millisecond,
|
|
healthCheckInterval: 50 * time.Millisecond,
|
|
}
|
|
|
|
// Command that runs for a while then exits
|
|
testCmd := exec.CommandContext(ctx, "sleep", "1")
|
|
supervisor.cmd = testCmd
|
|
|
|
var wg sync.WaitGroup
|
|
wg.Add(1)
|
|
|
|
go func() {
|
|
defer wg.Done()
|
|
supervisor.Start(ctx)
|
|
}()
|
|
|
|
// Initially should be running
|
|
time.Sleep(200 * time.Millisecond)
|
|
assert.True(t, supervisor.IsRunning(), "Supervisor should be running initially")
|
|
|
|
// Wait for process to exit and health check to detect it
|
|
time.Sleep(1.5 * time.Second)
|
|
|
|
// Should have detected process exit and attempted restart
|
|
assert.True(t, supervisor.GetRestartCount() > 0, "Health monitoring should detect process exit")
|
|
|
|
cancel()
|
|
wg.Wait()
|
|
}
|
|
|
|
// TestAudioInputSupervisorIntegration tests the actual AudioInputSupervisor
|
|
func TestAudioInputSupervisorIntegration(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("Skipping integration test in short mode")
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
|
defer cancel()
|
|
|
|
// Create a real supervisor instance
|
|
supervisor := NewAudioInputSupervisor()
|
|
require.NotNil(t, supervisor, "Supervisor should be created")
|
|
|
|
// Test that supervisor can be started and stopped cleanly
|
|
var wg sync.WaitGroup
|
|
wg.Add(1)
|
|
|
|
go func() {
|
|
defer wg.Done()
|
|
// This will likely fail due to missing audio hardware in test environment,
|
|
// but we're testing the supervisor logic, not the audio functionality
|
|
supervisor.Start(ctx)
|
|
}()
|
|
|
|
// Let it run briefly
|
|
time.Sleep(500 * time.Millisecond)
|
|
|
|
// Stop the supervisor
|
|
cancel()
|
|
wg.Wait()
|
|
|
|
// Verify clean shutdown
|
|
assert.False(t, supervisor.IsRunning(), "Supervisor should not be running after context cancellation")
|
|
}
|
|
|
|
// Mock supervisor for testing (simplified version)
|
|
type AudioInputSupervisor struct {
|
|
logger zerolog.Logger
|
|
cmd *exec.Cmd
|
|
maxRestarts int
|
|
restartDelay time.Duration
|
|
healthCheckInterval time.Duration
|
|
restartCount int
|
|
running bool
|
|
mu sync.RWMutex
|
|
restart func() // Hook for testing
|
|
}
|
|
|
|
func (s *AudioInputSupervisor) Start(ctx context.Context) error {
|
|
s.mu.Lock()
|
|
s.running = true
|
|
s.mu.Unlock()
|
|
|
|
for s.restartCount < s.maxRestarts {
|
|
select {
|
|
case <-ctx.Done():
|
|
s.mu.Lock()
|
|
s.running = false
|
|
s.mu.Unlock()
|
|
return ctx.Err()
|
|
default:
|
|
}
|
|
|
|
// Start process
|
|
if s.cmd != nil {
|
|
err := s.cmd.Start()
|
|
if err != nil {
|
|
s.logger.Error().Err(err).Msg("Failed to start process")
|
|
s.restartCount++
|
|
time.Sleep(s.getBackoffDelay())
|
|
continue
|
|
}
|
|
|
|
// Wait for process to exit
|
|
err = s.cmd.Wait()
|
|
if err != nil {
|
|
s.logger.Error().Err(err).Msg("Process exited with error")
|
|
}
|
|
}
|
|
|
|
s.restartCount++
|
|
if s.restart != nil {
|
|
s.restart()
|
|
}
|
|
|
|
if s.restartCount < s.maxRestarts {
|
|
time.Sleep(s.getBackoffDelay())
|
|
}
|
|
}
|
|
|
|
s.mu.Lock()
|
|
s.running = false
|
|
s.mu.Unlock()
|
|
return nil
|
|
}
|
|
|
|
func (s *AudioInputSupervisor) IsRunning() bool {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
return s.running
|
|
}
|
|
|
|
func (s *AudioInputSupervisor) GetRestartCount() int {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
return s.restartCount
|
|
}
|
|
|
|
func (s *AudioInputSupervisor) getBackoffDelay() time.Duration {
|
|
// Simple exponential backoff
|
|
multiplier := 1 << uint(s.restartCount)
|
|
if multiplier > 8 {
|
|
multiplier = 8 // Cap the multiplier
|
|
}
|
|
return s.restartDelay * time.Duration(multiplier)
|
|
}
|
|
|
|
// NewAudioInputSupervisor creates a new supervisor for testing
|
|
func NewAudioInputSupervisor() *AudioInputSupervisor {
|
|
return &AudioInputSupervisor{
|
|
logger: getTestLogger(),
|
|
maxRestarts: getMaxRestartAttempts(),
|
|
restartDelay: getInitialRestartDelay(),
|
|
healthCheckInterval: 1 * time.Second,
|
|
}
|
|
} |