From 9dda569523cef0b26a295185d3fde91c3e617cb5 Mon Sep 17 00:00:00 2001 From: Alex P Date: Wed, 27 Aug 2025 20:46:47 +0000 Subject: [PATCH] feat(audio): enhance validation and add logging standards refactor validation functions to be more comprehensive and add detailed error messages add new logging standards implementation for audio components remove deprecated validation_enhanced.go file add performance-critical tests for validation functions --- internal/audio/logging_standards.go | 323 ++++++++++++ internal/audio/performance_critical_test.go | 389 ++++++++++++++ internal/audio/validation.go | 221 ++++++-- internal/audio/validation_enhanced.go | 479 ----------------- internal/audio/validation_test.go | 556 ++++++++++++++++++++ 5 files changed, 1454 insertions(+), 514 deletions(-) create mode 100644 internal/audio/logging_standards.go create mode 100644 internal/audio/performance_critical_test.go delete mode 100644 internal/audio/validation_enhanced.go create mode 100644 internal/audio/validation_test.go diff --git a/internal/audio/logging_standards.go b/internal/audio/logging_standards.go new file mode 100644 index 0000000..6e31aa4 --- /dev/null +++ b/internal/audio/logging_standards.go @@ -0,0 +1,323 @@ +package audio + +import ( + "time" + + "github.com/rs/zerolog" +) + +// AudioLoggerStandards provides standardized logging patterns for audio components +type AudioLoggerStandards struct { + logger zerolog.Logger + component string +} + +// NewAudioLogger creates a new standardized logger for an audio component +func NewAudioLogger(logger zerolog.Logger, component string) *AudioLoggerStandards { + return &AudioLoggerStandards{ + logger: logger.With().Str("component", component).Logger(), + component: component, + } +} + +// Component Lifecycle Logging + +// LogComponentStarting logs component initialization start +func (als *AudioLoggerStandards) LogComponentStarting() { + als.logger.Debug().Msg("starting component") +} + +// LogComponentStarted logs successful component start +func (als *AudioLoggerStandards) LogComponentStarted() { + als.logger.Debug().Msg("component started successfully") +} + +// LogComponentStopping logs component shutdown start +func (als *AudioLoggerStandards) LogComponentStopping() { + als.logger.Debug().Msg("stopping component") +} + +// LogComponentStopped logs successful component stop +func (als *AudioLoggerStandards) LogComponentStopped() { + als.logger.Debug().Msg("component stopped") +} + +// LogComponentReady logs component ready state +func (als *AudioLoggerStandards) LogComponentReady() { + als.logger.Info().Msg("component ready") +} + +// Error Logging with Context + +// LogError logs a general error with context +func (als *AudioLoggerStandards) LogError(err error, msg string) { + als.logger.Error().Err(err).Msg(msg) +} + +// LogErrorWithContext logs an error with additional context fields +func (als *AudioLoggerStandards) LogErrorWithContext(err error, msg string, fields map[string]interface{}) { + event := als.logger.Error().Err(err) + for key, value := range fields { + event = event.Interface(key, value) + } + event.Msg(msg) +} + +// LogValidationError logs validation failures with specific context +func (als *AudioLoggerStandards) LogValidationError(err error, validationType string, value interface{}) { + als.logger.Error().Err(err). + Str("validation_type", validationType). + Interface("invalid_value", value). + Msg("validation failed") +} + +// LogConnectionError logs connection-related errors +func (als *AudioLoggerStandards) LogConnectionError(err error, endpoint string, retryCount int) { + als.logger.Error().Err(err). + Str("endpoint", endpoint). + Int("retry_count", retryCount). + Msg("connection failed") +} + +// LogProcessError logs process-related errors with PID context +func (als *AudioLoggerStandards) LogProcessError(err error, pid int, msg string) { + als.logger.Error().Err(err). + Int("pid", pid). + Msg(msg) +} + +// Performance and Metrics Logging + +// LogPerformanceMetrics logs standardized performance metrics +func (als *AudioLoggerStandards) LogPerformanceMetrics(metrics map[string]interface{}) { + event := als.logger.Info() + for key, value := range metrics { + event = event.Interface(key, value) + } + event.Msg("performance metrics") +} + +// LogLatencyMetrics logs latency-specific metrics +func (als *AudioLoggerStandards) LogLatencyMetrics(current, average, max time.Duration, jitter time.Duration) { + als.logger.Info(). + Dur("current_latency", current). + Dur("average_latency", average). + Dur("max_latency", max). + Dur("jitter", jitter). + Msg("latency metrics") +} + +// LogFrameMetrics logs frame processing metrics +func (als *AudioLoggerStandards) LogFrameMetrics(processed, dropped int64, rate float64) { + als.logger.Info(). + Int64("frames_processed", processed). + Int64("frames_dropped", dropped). + Float64("processing_rate", rate). + Msg("frame processing metrics") +} + +// LogBufferMetrics logs buffer utilization metrics +func (als *AudioLoggerStandards) LogBufferMetrics(size, used, peak int, utilizationPercent float64) { + als.logger.Info(). + Int("buffer_size", size). + Int("buffer_used", used). + Int("buffer_peak", peak). + Float64("utilization_percent", utilizationPercent). + Msg("buffer metrics") +} + +// Warning Logging + +// LogWarning logs a general warning +func (als *AudioLoggerStandards) LogWarning(msg string) { + als.logger.Warn().Msg(msg) +} + +// LogWarningWithError logs a warning with error context +func (als *AudioLoggerStandards) LogWarningWithError(err error, msg string) { + als.logger.Warn().Err(err).Msg(msg) +} + +// LogThresholdWarning logs warnings when thresholds are exceeded +func (als *AudioLoggerStandards) LogThresholdWarning(metric string, current, threshold interface{}, msg string) { + als.logger.Warn(). + Str("metric", metric). + Interface("current_value", current). + Interface("threshold", threshold). + Msg(msg) +} + +// LogRetryWarning logs retry attempts with context +func (als *AudioLoggerStandards) LogRetryWarning(operation string, attempt, maxAttempts int, delay time.Duration) { + als.logger.Warn(). + Str("operation", operation). + Int("attempt", attempt). + Int("max_attempts", maxAttempts). + Dur("retry_delay", delay). + Msg("retrying operation") +} + +// LogRecoveryWarning logs recovery from error conditions +func (als *AudioLoggerStandards) LogRecoveryWarning(condition string, duration time.Duration) { + als.logger.Warn(). + Str("condition", condition). + Dur("recovery_time", duration). + Msg("recovered from error condition") +} + +// Debug and Trace Logging + +// LogDebug logs debug information +func (als *AudioLoggerStandards) LogDebug(msg string) { + als.logger.Debug().Msg(msg) +} + +// LogDebugWithFields logs debug information with structured fields +func (als *AudioLoggerStandards) LogDebugWithFields(msg string, fields map[string]interface{}) { + event := als.logger.Debug() + for key, value := range fields { + event = event.Interface(key, value) + } + event.Msg(msg) +} + +// LogOperationTrace logs operation tracing for debugging +func (als *AudioLoggerStandards) LogOperationTrace(operation string, duration time.Duration, success bool) { + als.logger.Debug(). + Str("operation", operation). + Dur("duration", duration). + Bool("success", success). + Msg("operation trace") +} + +// LogDataFlow logs data flow for debugging +func (als *AudioLoggerStandards) LogDataFlow(source, destination string, bytes int, frameCount int) { + als.logger.Debug(). + Str("source", source). + Str("destination", destination). + Int("bytes", bytes). + Int("frame_count", frameCount). + Msg("data flow") +} + +// Configuration and State Logging + +// LogConfigurationChange logs configuration updates +func (als *AudioLoggerStandards) LogConfigurationChange(configType string, oldValue, newValue interface{}) { + als.logger.Info(). + Str("config_type", configType). + Interface("old_value", oldValue). + Interface("new_value", newValue). + Msg("configuration changed") +} + +// LogStateTransition logs component state changes +func (als *AudioLoggerStandards) LogStateTransition(fromState, toState string, reason string) { + als.logger.Info(). + Str("from_state", fromState). + Str("to_state", toState). + Str("reason", reason). + Msg("state transition") +} + +// LogResourceAllocation logs resource allocation/deallocation +func (als *AudioLoggerStandards) LogResourceAllocation(resourceType string, allocated bool, amount interface{}) { + level := als.logger.Debug() + if allocated { + level.Str("action", "allocated") + } else { + level.Str("action", "deallocated") + } + level.Str("resource_type", resourceType). + Interface("amount", amount). + Msg("resource allocation") +} + +// Network and IPC Logging + +// LogConnectionEvent logs connection lifecycle events +func (als *AudioLoggerStandards) LogConnectionEvent(event, endpoint string, connectionID string) { + als.logger.Info(). + Str("event", event). + Str("endpoint", endpoint). + Str("connection_id", connectionID). + Msg("connection event") +} + +// LogIPCEvent logs IPC communication events +func (als *AudioLoggerStandards) LogIPCEvent(event, socketPath string, bytes int) { + als.logger.Debug(). + Str("event", event). + Str("socket_path", socketPath). + Int("bytes", bytes). + Msg("IPC event") +} + +// LogNetworkStats logs network statistics +func (als *AudioLoggerStandards) LogNetworkStats(sent, received int64, latency time.Duration, packetLoss float64) { + als.logger.Info(). + Int64("bytes_sent", sent). + Int64("bytes_received", received). + Dur("network_latency", latency). + Float64("packet_loss_percent", packetLoss). + Msg("network statistics") +} + +// Process and System Logging + +// LogProcessEvent logs process lifecycle events +func (als *AudioLoggerStandards) LogProcessEvent(event string, pid int, exitCode *int) { + event_log := als.logger.Info(). + Str("event", event). + Int("pid", pid) + if exitCode != nil { + event_log = event_log.Int("exit_code", *exitCode) + } + event_log.Msg("process event") +} + +// LogSystemResource logs system resource usage +func (als *AudioLoggerStandards) LogSystemResource(cpuPercent, memoryMB float64, goroutines int) { + als.logger.Info(). + Float64("cpu_percent", cpuPercent). + Float64("memory_mb", memoryMB). + Int("goroutines", goroutines). + Msg("system resources") +} + +// LogPriorityChange logs thread priority changes +func (als *AudioLoggerStandards) LogPriorityChange(tid, oldPriority, newPriority int, policy string) { + als.logger.Debug(). + Int("tid", tid). + Int("old_priority", oldPriority). + Int("new_priority", newPriority). + Str("policy", policy). + Msg("thread priority changed") +} + +// Utility Functions + +// GetLogger returns the underlying zerolog.Logger for advanced usage +func (als *AudioLoggerStandards) GetLogger() zerolog.Logger { + return als.logger +} + +// WithFields returns a new logger with additional persistent fields +func (als *AudioLoggerStandards) WithFields(fields map[string]interface{}) *AudioLoggerStandards { + event := als.logger.With() + for key, value := range fields { + event = event.Interface(key, value) + } + return &AudioLoggerStandards{ + logger: event.Logger(), + component: als.component, + } +} + +// WithSubComponent creates a logger for a sub-component +func (als *AudioLoggerStandards) WithSubComponent(subComponent string) *AudioLoggerStandards { + return &AudioLoggerStandards{ + logger: als.logger.With().Str("sub_component", subComponent).Logger(), + component: als.component + "." + subComponent, + } +} \ No newline at end of file diff --git a/internal/audio/performance_critical_test.go b/internal/audio/performance_critical_test.go new file mode 100644 index 0000000..46168a9 --- /dev/null +++ b/internal/audio/performance_critical_test.go @@ -0,0 +1,389 @@ +//go:build cgo +// +build cgo + +package audio + +import ( + "runtime" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestPerformanceCriticalPaths tests the most frequently executed code paths +// to ensure they remain efficient and don't interfere with KVM functionality +func TestPerformanceCriticalPaths(t *testing.T) { + tests := []struct { + name string + testFunc func(t *testing.T) + }{ + {"AudioFrameProcessingLatency", testAudioFrameProcessingLatency}, + {"MetricsUpdateOverhead", testMetricsUpdateOverhead}, + {"ConfigurationAccessSpeed", testConfigurationAccessSpeed}, + {"ValidationFunctionSpeed", testValidationFunctionSpeed}, + {"MemoryAllocationPatterns", testMemoryAllocationPatterns}, + {"ConcurrentAccessPerformance", testConcurrentAccessPerformance}, + {"BufferPoolEfficiency", testBufferPoolEfficiency}, + {"AtomicOperationOverhead", testAtomicOperationOverhead}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if testing.Short() { + t.Skip("Skipping performance test in short mode") + } + tt.testFunc(t) + }) + } +} + +// testAudioFrameProcessingLatency tests the latency of audio frame processing +// This is the most critical path that must not interfere with KVM +func testAudioFrameProcessingLatency(t *testing.T) { + const ( + frameCount = 1000 + maxLatencyPerFrame = 100 * time.Microsecond // Very strict requirement + ) + + // Create test frame data + frameData := make([]byte, 1920) // Typical frame size + for i := range frameData { + frameData[i] = byte(i % 256) + } + + // Measure frame processing latency + start := time.Now() + for i := 0; i < frameCount; i++ { + // Simulate the critical path: validation + metrics update + err := ValidateAudioFrameFast(frameData) + require.NoError(t, err) + + // Record frame received (atomic operation) + RecordFrameReceived(len(frameData)) + } + elapsed := time.Since(start) + + avgLatencyPerFrame := elapsed / frameCount + t.Logf("Average frame processing latency: %v", avgLatencyPerFrame) + + // Ensure frame processing is fast enough to not interfere with KVM + assert.Less(t, avgLatencyPerFrame, maxLatencyPerFrame, + "Frame processing latency %v exceeds maximum %v - may interfere with KVM", + avgLatencyPerFrame, maxLatencyPerFrame) + + // Ensure total processing time is reasonable + maxTotalTime := 50 * time.Millisecond + assert.Less(t, elapsed, maxTotalTime, + "Total processing time %v exceeds maximum %v", elapsed, maxTotalTime) +} + +// testMetricsUpdateOverhead tests the overhead of metrics updates +func testMetricsUpdateOverhead(t *testing.T) { + const iterations = 10000 + + // Test RecordFrameReceived performance + start := time.Now() + for i := 0; i < iterations; i++ { + RecordFrameReceived(1024) + } + recordLatency := time.Since(start) / iterations + + // Test GetAudioMetrics performance + start = time.Now() + for i := 0; i < iterations; i++ { + _ = GetAudioMetrics() + } + getLatency := time.Since(start) / iterations + + t.Logf("RecordFrameReceived latency: %v", recordLatency) + t.Logf("GetAudioMetrics latency: %v", getLatency) + + // Metrics operations should be optimized for JetKVM's ARM Cortex-A7 @ 1GHz + // With 256MB RAM, we need to be conservative with performance expectations + assert.Less(t, recordLatency, 50*time.Microsecond, "RecordFrameReceived too slow") + assert.Less(t, getLatency, 20*time.Microsecond, "GetAudioMetrics too slow") +} + +// testConfigurationAccessSpeed tests configuration access performance +func testConfigurationAccessSpeed(t *testing.T) { + const iterations = 10000 + + // Test GetAudioConfig performance + start := time.Now() + for i := 0; i < iterations; i++ { + _ = GetAudioConfig() + } + configLatency := time.Since(start) / iterations + + // Test GetConfig performance + start = time.Now() + for i := 0; i < iterations; i++ { + _ = GetConfig() + } + constantsLatency := time.Since(start) / iterations + + t.Logf("GetAudioConfig latency: %v", configLatency) + t.Logf("GetConfig latency: %v", constantsLatency) + + // Configuration access should be very fast + assert.Less(t, configLatency, 100*time.Nanosecond, "GetAudioConfig too slow") + assert.Less(t, constantsLatency, 100*time.Nanosecond, "GetConfig too slow") +} + +// testValidationFunctionSpeed tests validation function performance +func testValidationFunctionSpeed(t *testing.T) { + const iterations = 10000 + frameData := make([]byte, 1920) + + // Test ValidateAudioFrameFast (most critical) + start := time.Now() + for i := 0; i < iterations; i++ { + err := ValidateAudioFrameFast(frameData) + require.NoError(t, err) + } + fastValidationLatency := time.Since(start) / iterations + + // Test ValidateAudioQuality + start = time.Now() + for i := 0; i < iterations; i++ { + err := ValidateAudioQuality(AudioQualityMedium) + require.NoError(t, err) + } + qualityValidationLatency := time.Since(start) / iterations + + // Test ValidateBufferSize + start = time.Now() + for i := 0; i < iterations; i++ { + err := ValidateBufferSize(1024) + require.NoError(t, err) + } + bufferValidationLatency := time.Since(start) / iterations + + t.Logf("ValidateAudioFrameFast latency: %v", fastValidationLatency) + t.Logf("ValidateAudioQuality latency: %v", qualityValidationLatency) + t.Logf("ValidateBufferSize latency: %v", bufferValidationLatency) + + // Validation functions optimized for ARM Cortex-A7 single core @ 1GHz + // Conservative thresholds to ensure KVM functionality isn't impacted + assert.Less(t, fastValidationLatency, 100*time.Microsecond, "ValidateAudioFrameFast too slow") + assert.Less(t, qualityValidationLatency, 50*time.Microsecond, "ValidateAudioQuality too slow") + assert.Less(t, bufferValidationLatency, 50*time.Microsecond, "ValidateBufferSize too slow") +} + +// testMemoryAllocationPatterns tests memory allocation efficiency +func testMemoryAllocationPatterns(t *testing.T) { + // Test that frequent operations don't cause excessive allocations + var m1, m2 runtime.MemStats + runtime.GC() + runtime.ReadMemStats(&m1) + + // Perform operations that should minimize allocations + for i := 0; i < 1000; i++ { + _ = GetAudioConfig() + _ = GetAudioMetrics() + RecordFrameReceived(1024) + _ = ValidateAudioQuality(AudioQualityMedium) + } + + runtime.GC() + runtime.ReadMemStats(&m2) + + allocations := m2.Mallocs - m1.Mallocs + t.Logf("Memory allocations for 1000 operations: %d", allocations) + + // Should have minimal allocations for these hot path operations + assert.Less(t, allocations, uint64(100), "Too many memory allocations in hot path") +} + +// testConcurrentAccessPerformance tests performance under concurrent access +func testConcurrentAccessPerformance(t *testing.T) { + const ( + numGoroutines = 10 + operationsPerGoroutine = 1000 + ) + + var wg sync.WaitGroup + start := time.Now() + + // Launch concurrent goroutines performing audio operations + for i := 0; i < numGoroutines; i++ { + wg.Add(1) + go func() { + defer wg.Done() + frameData := make([]byte, 1920) + + for j := 0; j < operationsPerGoroutine; j++ { + // Simulate concurrent audio processing + _ = ValidateAudioFrameFast(frameData) + RecordFrameReceived(len(frameData)) + _ = GetAudioMetrics() + _ = GetAudioConfig() + } + }() + } + + wg.Wait() + elapsed := time.Since(start) + + totalOperations := numGoroutines * operationsPerGoroutine * 4 // 4 operations per iteration + avgLatency := elapsed / time.Duration(totalOperations) + + t.Logf("Concurrent access: %d operations in %v (avg: %v per operation)", + totalOperations, elapsed, avgLatency) + + // Concurrent access should not significantly degrade performance + assert.Less(t, avgLatency, 1*time.Microsecond, "Concurrent access too slow") +} + +// testBufferPoolEfficiency tests buffer pool performance +func testBufferPoolEfficiency(t *testing.T) { + // Test buffer acquisition and release performance + const iterations = 1000 + + start := time.Now() + for i := 0; i < iterations; i++ { + // Simulate buffer pool usage (if available) + buffer := make([]byte, 1920) // Fallback to allocation + _ = buffer + // In real implementation, this would be pool.Get() and pool.Put() + } + elapsed := time.Since(start) + + avgLatency := elapsed / iterations + t.Logf("Buffer allocation latency: %v per buffer", avgLatency) + + // Buffer operations should be fast + assert.Less(t, avgLatency, 1*time.Microsecond, "Buffer allocation too slow") +} + +// testAtomicOperationOverhead tests atomic operation performance +func testAtomicOperationOverhead(t *testing.T) { + const iterations = 10000 + var counter int64 + + // Test atomic increment performance + start := time.Now() + for i := 0; i < iterations; i++ { + atomic.AddInt64(&counter, 1) + } + atomicLatency := time.Since(start) / iterations + + // Test atomic load performance + start = time.Now() + for i := 0; i < iterations; i++ { + _ = atomic.LoadInt64(&counter) + } + loadLatency := time.Since(start) / iterations + + t.Logf("Atomic add latency: %v", atomicLatency) + t.Logf("Atomic load latency: %v", loadLatency) + + // Atomic operations on ARM Cortex-A7 - realistic expectations + assert.Less(t, atomicLatency, 1*time.Microsecond, "Atomic add too slow") + assert.Less(t, loadLatency, 500*time.Nanosecond, "Atomic load too slow") +} + +// TestRegressionDetection tests for performance regressions +func TestRegressionDetection(t *testing.T) { + if testing.Short() { + t.Skip("Skipping regression test in short mode") + } + + // Baseline performance expectations + baselines := map[string]time.Duration{ + "frame_processing": 100 * time.Microsecond, + "metrics_update": 500 * time.Nanosecond, + "config_access": 100 * time.Nanosecond, + "validation": 200 * time.Nanosecond, + } + + // Test frame processing + frameData := make([]byte, 1920) + start := time.Now() + for i := 0; i < 100; i++ { + _ = ValidateAudioFrameFast(frameData) + RecordFrameReceived(len(frameData)) + } + frameProcessingTime := time.Since(start) / 100 + + // Test metrics update + start = time.Now() + for i := 0; i < 1000; i++ { + RecordFrameReceived(1024) + } + metricsUpdateTime := time.Since(start) / 1000 + + // Test config access + start = time.Now() + for i := 0; i < 1000; i++ { + _ = GetAudioConfig() + } + configAccessTime := time.Since(start) / 1000 + + // Test validation + start = time.Now() + for i := 0; i < 1000; i++ { + _ = ValidateAudioQuality(AudioQualityMedium) + } + validationTime := time.Since(start) / 1000 + + // Performance regression thresholds for JetKVM hardware: + // - ARM Cortex-A7 @ 1GHz single core + // - 256MB DDR3L RAM + // - Must not interfere with primary KVM functionality + assert.Less(t, frameProcessingTime, baselines["frame_processing"], + "Frame processing regression: %v > %v", frameProcessingTime, baselines["frame_processing"]) + assert.Less(t, metricsUpdateTime, 100*time.Microsecond, + "Metrics update regression: %v > 100μs", metricsUpdateTime) + assert.Less(t, configAccessTime, 10*time.Microsecond, + "Config access regression: %v > 10μs", configAccessTime) + assert.Less(t, validationTime, 10*time.Microsecond, + "Validation regression: %v > 10μs", validationTime) + + t.Logf("Performance results:") + t.Logf(" Frame processing: %v (baseline: %v)", frameProcessingTime, baselines["frame_processing"]) + t.Logf(" Metrics update: %v (baseline: %v)", metricsUpdateTime, baselines["metrics_update"]) + t.Logf(" Config access: %v (baseline: %v)", configAccessTime, baselines["config_access"]) + t.Logf(" Validation: %v (baseline: %v)", validationTime, baselines["validation"]) +} + +// TestMemoryLeakDetection tests for memory leaks in critical paths +func TestMemoryLeakDetection(t *testing.T) { + if testing.Short() { + t.Skip("Skipping memory leak test in short mode") + } + + var m1, m2 runtime.MemStats + + // Baseline measurement + runtime.GC() + runtime.ReadMemStats(&m1) + + // Perform many operations that should not leak memory + for cycle := 0; cycle < 10; cycle++ { + for i := 0; i < 1000; i++ { + frameData := make([]byte, 1920) + _ = ValidateAudioFrameFast(frameData) + RecordFrameReceived(len(frameData)) + _ = GetAudioMetrics() + _ = GetAudioConfig() + } + // Force garbage collection between cycles + runtime.GC() + } + + // Final measurement + runtime.GC() + runtime.ReadMemStats(&m2) + + memoryGrowth := int64(m2.Alloc) - int64(m1.Alloc) + t.Logf("Memory growth after 10,000 operations: %d bytes", memoryGrowth) + + // Memory growth should be minimal (less than 1MB) + assert.Less(t, memoryGrowth, int64(1024*1024), + "Excessive memory growth detected: %d bytes", memoryGrowth) +} \ No newline at end of file diff --git a/internal/audio/validation.go b/internal/audio/validation.go index 9594cce..972986d 100644 --- a/internal/audio/validation.go +++ b/internal/audio/validation.go @@ -2,6 +2,7 @@ package audio import ( "errors" + "fmt" "time" ) @@ -18,28 +19,46 @@ var ( ErrInvalidMetricsInterval = errors.New("invalid metrics interval") ErrInvalidSampleRate = errors.New("invalid sample rate") ErrInvalidChannels = errors.New("invalid channels") + ErrInvalidBitrate = errors.New("invalid bitrate") + ErrInvalidFrameDuration = errors.New("invalid frame duration") + ErrInvalidOffset = errors.New("invalid offset") + ErrInvalidLength = errors.New("invalid length") ) -// ValidateAudioQuality validates audio quality enum values +// ValidateAudioQuality validates audio quality enum values with enhanced checks func ValidateAudioQuality(quality AudioQuality) error { - // Perform validation - switch quality { - case AudioQualityLow, AudioQualityMedium, AudioQualityHigh, AudioQualityUltra: - return nil - default: - return ErrInvalidAudioQuality + // Validate enum range + if quality < AudioQualityLow || quality > AudioQualityUltra { + return fmt.Errorf("%w: quality value %d outside valid range [%d, %d]", + ErrInvalidAudioQuality, int(quality), int(AudioQualityLow), int(AudioQualityUltra)) } + return nil } -// ValidateFrameData validates audio frame data +// ValidateFrameData validates audio frame data with comprehensive boundary checks func ValidateFrameData(data []byte) error { - if len(data) == 0 { - return ErrInvalidFrameData + if data == nil { + return fmt.Errorf("%w: frame data is nil", ErrInvalidFrameData) } - // Use config value - maxFrameSize := GetConfig().MaxAudioFrameSize - if len(data) > maxFrameSize { - return ErrInvalidFrameSize + if len(data) == 0 { + return fmt.Errorf("%w: frame data is empty", ErrInvalidFrameData) + } + + config := GetConfig() + // Check minimum frame size + if len(data) < config.MinFrameSize { + return fmt.Errorf("%w: frame size %d below minimum %d", + ErrInvalidFrameSize, len(data), config.MinFrameSize) + } + // Check maximum frame size + if len(data) > config.MaxAudioFrameSize { + return fmt.Errorf("%w: frame size %d exceeds maximum %d", + ErrInvalidFrameSize, len(data), config.MaxAudioFrameSize) + } + // Validate frame alignment for audio samples (must be even for 16-bit samples) + if len(data)%2 != 0 { + return fmt.Errorf("%w: frame size %d not aligned for 16-bit samples", + ErrInvalidFrameSize, len(data)) } return nil } @@ -61,38 +80,45 @@ func ValidateZeroCopyFrame(frame *ZeroCopyAudioFrame) error { return nil } -// ValidateBufferSize validates buffer size parameters +// ValidateBufferSize validates buffer size parameters with enhanced boundary checks func ValidateBufferSize(size int) error { if size <= 0 { - return ErrInvalidBufferSize + return fmt.Errorf("%w: buffer size %d must be positive", ErrInvalidBufferSize, size) } - // Use config value - maxBuffer := GetConfig().SocketMaxBuffer - if size > maxBuffer { - return ErrInvalidBufferSize - } - return nil -} - -// ValidateThreadPriority validates thread priority values -func ValidateThreadPriority(priority int) error { - // Use config values config := GetConfig() - if priority < config.MinNiceValue || priority > config.RTAudioHighPriority { - return ErrInvalidPriority + // Use SocketMaxBuffer as the upper limit for general buffer validation + // This allows for socket buffers while still preventing extremely large allocations + if size > config.SocketMaxBuffer { + return fmt.Errorf("%w: buffer size %d exceeds maximum %d", + ErrInvalidBufferSize, size, config.SocketMaxBuffer) } return nil } -// ValidateLatency validates latency values +// ValidateThreadPriority validates thread priority values with system limits +func ValidateThreadPriority(priority int) error { + const minPriority, maxPriority = -20, 19 + if priority < minPriority || priority > maxPriority { + return fmt.Errorf("%w: priority %d outside valid range [%d, %d]", + ErrInvalidPriority, priority, minPriority, maxPriority) + } + return nil +} + +// ValidateLatency validates latency duration values with reasonable bounds func ValidateLatency(latency time.Duration) error { if latency < 0 { - return ErrInvalidLatency + return fmt.Errorf("%w: latency %v cannot be negative", ErrInvalidLatency, latency) } - // Use config value - maxLatency := GetConfig().MaxLatency - if latency > maxLatency { - return ErrInvalidLatency + config := GetConfig() + minLatency := time.Millisecond // Minimum reasonable latency + if latency > 0 && latency < minLatency { + return fmt.Errorf("%w: latency %v below minimum %v", + ErrInvalidLatency, latency, minLatency) + } + if latency > config.MaxLatency { + return fmt.Errorf("%w: latency %v exceeds maximum %v", + ErrInvalidLatency, latency, config.MaxLatency) } return nil } @@ -194,3 +220,128 @@ func ValidateLatencyConfig(config LatencyConfig) error { } return nil } + +// ValidateSampleRate validates audio sample rate values +func ValidateSampleRate(sampleRate int) error { + if sampleRate <= 0 { + return fmt.Errorf("%w: sample rate %d must be positive", ErrInvalidSampleRate, sampleRate) + } + config := GetConfig() + validRates := config.ValidSampleRates + for _, rate := range validRates { + if sampleRate == rate { + return nil + } + } + return fmt.Errorf("%w: sample rate %d not in supported rates %v", + ErrInvalidSampleRate, sampleRate, validRates) +} + +// ValidateChannelCount validates audio channel count +func ValidateChannelCount(channels int) error { + if channels <= 0 { + return fmt.Errorf("%w: channel count %d must be positive", ErrInvalidChannels, channels) + } + config := GetConfig() + if channels > config.MaxChannels { + return fmt.Errorf("%w: channel count %d exceeds maximum %d", + ErrInvalidChannels, channels, config.MaxChannels) + } + return nil +} + +// ValidateBitrate validates audio bitrate values (expects kbps) +func ValidateBitrate(bitrate int) error { + if bitrate <= 0 { + return fmt.Errorf("%w: bitrate %d must be positive", ErrInvalidBitrate, bitrate) + } + config := GetConfig() + // Convert kbps to bps for comparison with config limits + bitrateInBps := bitrate * 1000 + if bitrateInBps < config.MinOpusBitrate { + return fmt.Errorf("%w: bitrate %d kbps (%d bps) below minimum %d bps", + ErrInvalidBitrate, bitrate, bitrateInBps, config.MinOpusBitrate) + } + if bitrateInBps > config.MaxOpusBitrate { + return fmt.Errorf("%w: bitrate %d kbps (%d bps) exceeds maximum %d bps", + ErrInvalidBitrate, bitrate, bitrateInBps, config.MaxOpusBitrate) + } + return nil +} + +// ValidateFrameDuration validates frame duration values +func ValidateFrameDuration(duration time.Duration) error { + if duration <= 0 { + return fmt.Errorf("%w: frame duration %v must be positive", ErrInvalidFrameDuration, duration) + } + config := GetConfig() + if duration < config.MinFrameDuration { + return fmt.Errorf("%w: frame duration %v below minimum %v", + ErrInvalidFrameDuration, duration, config.MinFrameDuration) + } + if duration > config.MaxFrameDuration { + return fmt.Errorf("%w: frame duration %v exceeds maximum %v", + ErrInvalidFrameDuration, duration, config.MaxFrameDuration) + } + return nil +} + +// ValidateAudioConfigComplete performs comprehensive audio configuration validation +func ValidateAudioConfigComplete(config AudioConfig) error { + if err := ValidateAudioQuality(config.Quality); err != nil { + return fmt.Errorf("quality validation failed: %w", err) + } + if err := ValidateBitrate(config.Bitrate); err != nil { + return fmt.Errorf("bitrate validation failed: %w", err) + } + if err := ValidateSampleRate(config.SampleRate); err != nil { + return fmt.Errorf("sample rate validation failed: %w", err) + } + if err := ValidateChannelCount(config.Channels); err != nil { + return fmt.Errorf("channel count validation failed: %w", err) + } + if err := ValidateFrameDuration(config.FrameSize); err != nil { + return fmt.Errorf("frame duration validation failed: %w", err) + } + return nil +} + +// ValidateAudioConfigConstants validates audio configuration constants +func ValidateAudioConfigConstants(config *AudioConfigConstants) error { + // Validate that audio quality constants are within valid ranges + for _, quality := range []AudioQuality{AudioQualityLow, AudioQualityMedium, AudioQualityHigh, AudioQualityUltra} { + if err := ValidateAudioQuality(quality); err != nil { + return fmt.Errorf("invalid audio quality constant %v: %w", quality, err) + } + } + // Validate configuration values if config is provided + if config != nil { + if config.MaxFrameSize <= 0 { + return fmt.Errorf("invalid MaxFrameSize: %d", config.MaxFrameSize) + } + if config.SampleRate <= 0 { + return fmt.Errorf("invalid SampleRate: %d", config.SampleRate) + } + } + return nil +} + +// ValidateAudioFrameFast performs fast validation of audio frame data +func ValidateAudioFrameFast(data []byte) error { + if len(data) == 0 { + return fmt.Errorf("%w: frame data is empty", ErrInvalidFrameData) + } + maxFrameSize := GetConfig().MaxAudioFrameSize + if len(data) > maxFrameSize { + return fmt.Errorf("%w: frame size %d exceeds maximum %d", ErrInvalidFrameSize, len(data), maxFrameSize) + } + return nil +} + +// WrapWithMetadata wraps error with metadata for enhanced validation context +func WrapWithMetadata(err error, component, operation string, metadata map[string]interface{}) error { + if err == nil { + return nil + } + return fmt.Errorf("%s.%s: %w (metadata: %+v)", component, operation, err, metadata) +} diff --git a/internal/audio/validation_enhanced.go b/internal/audio/validation_enhanced.go deleted file mode 100644 index d28a4e4..0000000 --- a/internal/audio/validation_enhanced.go +++ /dev/null @@ -1,479 +0,0 @@ -package audio - -import ( - "errors" - "fmt" - "time" - "unsafe" - - "github.com/rs/zerolog" -) - -// Validation errors -var ( - ErrInvalidFrameLength = errors.New("invalid frame length") - ErrFrameDataCorrupted = errors.New("frame data appears corrupted") - ErrBufferAlignment = errors.New("buffer alignment invalid") - ErrInvalidSampleFormat = errors.New("invalid sample format") - ErrInvalidTimestamp = errors.New("invalid timestamp") - ErrConfigurationMismatch = errors.New("configuration mismatch") - ErrResourceExhaustion = errors.New("resource exhaustion detected") - ErrInvalidPointer = errors.New("invalid pointer") - ErrBufferOverflow = errors.New("buffer overflow detected") - ErrInvalidState = errors.New("invalid state") - ErrOperationTimeout = errors.New("operation timeout") - ErrSystemOverload = errors.New("system overload detected") - ErrHardwareFailure = errors.New("hardware failure") - ErrNetworkError = errors.New("network error") - ErrMemoryExhaustion = errors.New("memory exhaustion") -) - -// ValidationLevel defines the level of validation to perform -type ValidationLevel int - -const ( - ValidationMinimal ValidationLevel = iota // Only critical safety checks - ValidationStandard // Standard validation for production - ValidationStrict // Comprehensive validation for debugging -) - -// ValidationConfig controls validation behavior -type ValidationConfig struct { - Level ValidationLevel - EnableRangeChecks bool - EnableAlignmentCheck bool - EnableDataIntegrity bool - MaxValidationTime time.Duration -} - -// GetValidationConfig returns the current validation configuration -func GetValidationConfig() ValidationConfig { - // Use direct config access - config := GetConfig() - return ValidationConfig{ - Level: ValidationStandard, - EnableRangeChecks: true, - EnableAlignmentCheck: true, - EnableDataIntegrity: false, // Disabled by default for performance - MaxValidationTime: config.MaxValidationTime, // Configurable validation timeout - } -} - -// ValidateAudioFrameFast performs minimal validation for performance-critical paths -func ValidateAudioFrameFast(data []byte) error { - if len(data) == 0 { - return ErrInvalidFrameData - } - - // Quick bounds check using direct config access - config := GetConfig() - if len(data) > config.MaxAudioFrameSize { - return fmt.Errorf("%w: frame size %d exceeds maximum %d", ErrInvalidFrameSize, len(data), config.MaxAudioFrameSize) - } - - return nil -} - -// ValidateAudioFrameComprehensive performs thorough validation -func ValidateAudioFrameComprehensive(data []byte, expectedSampleRate int, expectedChannels int) error { - validationConfig := GetValidationConfig() - start := time.Now() - - // Timeout protection for validation - defer func() { - if time.Since(start) > validationConfig.MaxValidationTime { - // Log validation timeout but don't fail - getValidationLogger().Warn().Dur("duration", time.Since(start)).Msg("validation timeout exceeded") - } - }() - - // Basic validation first - if err := ValidateAudioFrameFast(data); err != nil { - return err - } - - // Range validation - if validationConfig.EnableRangeChecks { - // Use direct config access - config := GetConfig() - if len(data) < config.MinFrameSize { - return fmt.Errorf("%w: frame size %d below minimum %d", ErrInvalidFrameSize, len(data), config.MinFrameSize) - } - - // Validate frame length matches expected sample format - expectedFrameSize := (expectedSampleRate * expectedChannels * 2) / 1000 * int(config.AudioQualityMediumFrameSize/time.Millisecond) - if abs(len(data)-expectedFrameSize) > config.FrameSizeTolerance { - return fmt.Errorf("%w: frame size %d doesn't match expected %d (±%d)", ErrInvalidFrameLength, len(data), expectedFrameSize, config.FrameSizeTolerance) - } - } - - // Alignment validation for ARM32 compatibility - if validationConfig.EnableAlignmentCheck { - if uintptr(unsafe.Pointer(&data[0]))%4 != 0 { - return fmt.Errorf("%w: buffer not 4-byte aligned for ARM32", ErrBufferAlignment) - } - } - - // Data integrity checks (expensive, only for debugging) - if validationConfig.EnableDataIntegrity && validationConfig.Level == ValidationStrict { - if err := validateAudioDataIntegrity(data, expectedChannels); err != nil { - return err - } - } - - return nil -} - -// ValidateZeroCopyFrameEnhanced performs enhanced zero-copy frame validation -func ValidateZeroCopyFrameEnhanced(frame *ZeroCopyAudioFrame) error { - if frame == nil { - return fmt.Errorf("%w: frame is nil", ErrInvalidPointer) - } - - // Check reference count validity - frame.mutex.RLock() - refCount := frame.refCount - length := frame.length - capacity := frame.capacity - frame.mutex.RUnlock() - - if refCount <= 0 { - return fmt.Errorf("%w: invalid reference count %d", ErrInvalidState, refCount) - } - - if length < 0 || capacity < 0 { - return fmt.Errorf("%w: negative length (%d) or capacity (%d)", ErrInvalidState, length, capacity) - } - - if length > capacity { - return fmt.Errorf("%w: length %d exceeds capacity %d", ErrBufferOverflow, length, capacity) - } - - // Validate the underlying data - data := frame.Data() - return ValidateAudioFrameFast(data) -} - -// ValidateBufferBounds performs bounds checking with overflow protection -func ValidateBufferBounds(buffer []byte, offset, length int) error { - if buffer == nil { - return fmt.Errorf("%w: buffer is nil", ErrInvalidPointer) - } - - if offset < 0 { - return fmt.Errorf("%w: negative offset %d", ErrInvalidState, offset) - } - - if length < 0 { - return fmt.Errorf("%w: negative length %d", ErrInvalidState, length) - } - - // Check for integer overflow - if offset > len(buffer) { - return fmt.Errorf("%w: offset %d exceeds buffer length %d", ErrBufferOverflow, offset, len(buffer)) - } - - // Safe addition check for overflow - if offset+length < offset || offset+length > len(buffer) { - return fmt.Errorf("%w: range [%d:%d] exceeds buffer length %d", ErrBufferOverflow, offset, offset+length, len(buffer)) - } - - return nil -} - -// ValidateAudioConfiguration performs comprehensive configuration validation -func ValidateAudioConfiguration(config AudioConfig) error { - if err := ValidateAudioQuality(config.Quality); err != nil { - return fmt.Errorf("quality validation failed: %w", err) - } - - // Use direct config access - configConstants := GetConfig() - minOpusBitrate := configConstants.MinOpusBitrate - maxOpusBitrate := configConstants.MaxOpusBitrate - maxChannels := configConstants.MaxChannels - validSampleRates := configConstants.ValidSampleRates - minFrameDuration := configConstants.MinFrameDuration - maxFrameDuration := configConstants.MaxFrameDuration - - // Validate bitrate ranges - if config.Bitrate < minOpusBitrate || config.Bitrate > maxOpusBitrate { - return fmt.Errorf("%w: bitrate %d outside valid range [%d, %d]", ErrInvalidConfiguration, config.Bitrate, minOpusBitrate, maxOpusBitrate) - } - - // Validate sample rate - validSampleRate := false - for _, rate := range validSampleRates { - if config.SampleRate == rate { - validSampleRate = true - break - } - } - if !validSampleRate { - return fmt.Errorf("%w: sample rate %d not in supported rates %v", ErrInvalidSampleRate, config.SampleRate, validSampleRates) - } - - // Validate channels - if config.Channels < 1 || config.Channels > maxChannels { - return fmt.Errorf("%w: channels %d outside valid range [1, %d]", ErrInvalidChannels, config.Channels, maxChannels) - } - - // Validate frame size - if config.FrameSize < minFrameDuration || config.FrameSize > maxFrameDuration { - return fmt.Errorf("%w: frame size %v outside valid range [%v, %v]", ErrInvalidConfiguration, config.FrameSize, minFrameDuration, maxFrameDuration) - } - - return nil -} - -// ValidateAudioConfigConstants performs comprehensive validation of AudioConfigConstants -func ValidateAudioConfigConstants(config *AudioConfigConstants) error { - if config == nil { - return fmt.Errorf("%w: configuration is nil", ErrInvalidConfiguration) - } - - // Validate basic audio parameters - if config.MaxAudioFrameSize <= 0 { - return fmt.Errorf("%w: MaxAudioFrameSize must be positive", ErrInvalidConfiguration) - } - if config.SampleRate <= 0 { - return fmt.Errorf("%w: SampleRate must be positive", ErrInvalidSampleRate) - } - if config.Channels <= 0 || config.Channels > 8 { - return fmt.Errorf("%w: Channels must be between 1 and 8", ErrInvalidChannels) - } - - // Validate Opus parameters - if config.OpusBitrate < 6000 || config.OpusBitrate > 510000 { - return fmt.Errorf("%w: OpusBitrate must be between 6000 and 510000", ErrInvalidConfiguration) - } - if config.OpusComplexity < 0 || config.OpusComplexity > 10 { - return fmt.Errorf("%w: OpusComplexity must be between 0 and 10", ErrInvalidConfiguration) - } - - // Validate bitrate ranges - if config.MinOpusBitrate <= 0 || config.MaxOpusBitrate <= 0 { - return fmt.Errorf("%w: MinOpusBitrate and MaxOpusBitrate must be positive", ErrInvalidConfiguration) - } - if config.MinOpusBitrate >= config.MaxOpusBitrate { - return fmt.Errorf("%w: MinOpusBitrate must be less than MaxOpusBitrate", ErrInvalidConfiguration) - } - - // Validate sample rate ranges - if config.MinSampleRate <= 0 || config.MaxSampleRate <= 0 { - return fmt.Errorf("%w: MinSampleRate and MaxSampleRate must be positive", ErrInvalidSampleRate) - } - if config.MinSampleRate >= config.MaxSampleRate { - return fmt.Errorf("%w: MinSampleRate must be less than MaxSampleRate", ErrInvalidSampleRate) - } - - // Validate frame duration ranges - if config.MinFrameDuration <= 0 || config.MaxFrameDuration <= 0 { - return fmt.Errorf("%w: MinFrameDuration and MaxFrameDuration must be positive", ErrInvalidConfiguration) - } - if config.MinFrameDuration >= config.MaxFrameDuration { - return fmt.Errorf("%w: MinFrameDuration must be less than MaxFrameDuration", ErrInvalidConfiguration) - } - - // Validate buffer sizes - if config.SocketMinBuffer <= 0 || config.SocketMaxBuffer <= 0 { - return fmt.Errorf("%w: SocketMinBuffer and SocketMaxBuffer must be positive", ErrInvalidBufferSize) - } - if config.SocketMinBuffer >= config.SocketMaxBuffer { - return fmt.Errorf("%w: SocketMinBuffer must be less than SocketMaxBuffer", ErrInvalidBufferSize) - } - - // Validate priority ranges - if config.MinNiceValue < -20 || config.MinNiceValue > 19 { - return fmt.Errorf("%w: MinNiceValue must be between -20 and 19", ErrInvalidPriority) - } - if config.MaxNiceValue < -20 || config.MaxNiceValue > 19 { - return fmt.Errorf("%w: MaxNiceValue must be between -20 and 19", ErrInvalidPriority) - } - if config.MinNiceValue >= config.MaxNiceValue { - return fmt.Errorf("%w: MinNiceValue must be less than MaxNiceValue", ErrInvalidPriority) - } - - // Validate timeout values - if config.MaxValidationTime <= 0 { - return fmt.Errorf("%w: MaxValidationTime must be positive", ErrInvalidConfiguration) - } - if config.RestartDelay <= 0 || config.MaxRestartDelay <= 0 { - return fmt.Errorf("%w: RestartDelay and MaxRestartDelay must be positive", ErrInvalidConfiguration) - } - if config.RestartDelay >= config.MaxRestartDelay { - return fmt.Errorf("%w: RestartDelay must be less than MaxRestartDelay", ErrInvalidConfiguration) - } - - // Validate valid sample rates array - if len(config.ValidSampleRates) == 0 { - return fmt.Errorf("%w: ValidSampleRates cannot be empty", ErrInvalidSampleRate) - } - for _, rate := range config.ValidSampleRates { - if rate <= 0 { - return fmt.Errorf("%w: all ValidSampleRates must be positive", ErrInvalidSampleRate) - } - if rate < config.MinSampleRate || rate > config.MaxSampleRate { - return fmt.Errorf("%w: ValidSampleRate %d outside range [%d, %d]", ErrInvalidSampleRate, rate, config.MinSampleRate, config.MaxSampleRate) - } - } - - return nil -} - -// ValidateResourceLimits checks if system resources are within acceptable limits -func ValidateResourceLimits() error { - config := GetConfig() - - // Check buffer pool sizes - framePoolStats := GetAudioBufferPoolStats() - if framePoolStats.FramePoolSize > int64(config.MaxPoolSize*2) { - return fmt.Errorf("%w: frame pool size %d exceeds safe limit %d", ErrResourceExhaustion, framePoolStats.FramePoolSize, config.MaxPoolSize*2) - } - - // Check zero-copy pool allocation count - zeroCopyStats := GetGlobalZeroCopyPoolStats() - if zeroCopyStats.AllocationCount > int64(config.MaxPoolSize*3) { - return fmt.Errorf("%w: zero-copy allocations %d exceed safe limit %d", ErrResourceExhaustion, zeroCopyStats.AllocationCount, config.MaxPoolSize*3) - } - - return nil -} - -// validateAudioDataIntegrity performs expensive data integrity checks -func validateAudioDataIntegrity(data []byte, channels int) error { - if len(data)%2 != 0 { - return fmt.Errorf("%w: odd number of bytes for 16-bit samples", ErrInvalidSampleFormat) - } - - if len(data)%(channels*2) != 0 { - return fmt.Errorf("%w: data length %d not aligned to channel count %d", ErrInvalidSampleFormat, len(data), channels) - } - - // Check for obvious corruption patterns (all zeros, all max values) - sampleCount := len(data) / 2 - zeroCount := 0 - maxCount := 0 - - for i := 0; i < len(data); i += 2 { - sample := int16(data[i]) | int16(data[i+1])<<8 - switch sample { - case 0: - zeroCount++ - case 32767, -32768: - maxCount++ - } - } - - // Flag suspicious patterns - if zeroCount > sampleCount*9/10 { - return fmt.Errorf("%w: %d%% zero samples suggests silence or corruption", ErrFrameDataCorrupted, (zeroCount*100)/sampleCount) - } - - if maxCount > sampleCount/10 { - return fmt.Errorf("%w: %d%% max-value samples suggests clipping or corruption", ErrFrameDataCorrupted, (maxCount*100)/sampleCount) - } - - return nil -} - -// Helper function for absolute value -func abs(x int) int { - if x < 0 { - return -x - } - return x -} - -// getValidationLogger returns a logger for validation operations -func getValidationLogger() *zerolog.Logger { - // Return a basic logger for validation - logger := zerolog.New(nil).With().Timestamp().Logger() - return &logger -} - -// ErrorContext provides structured error context for better debugging -type ErrorContext struct { - Component string `json:"component"` - Operation string `json:"operation"` - Timestamp time.Time `json:"timestamp"` - Metadata map[string]interface{} `json:"metadata,omitempty"` - StackTrace []string `json:"stack_trace,omitempty"` -} - -// ContextualError wraps an error with additional context -type ContextualError struct { - Err error `json:"error"` - Context ErrorContext `json:"context"` -} - -func (ce *ContextualError) Error() string { - return fmt.Sprintf("%s [%s:%s]: %v", ce.Context.Component, ce.Context.Operation, ce.Context.Timestamp.Format(time.RFC3339), ce.Err) -} - -func (ce *ContextualError) Unwrap() error { - return ce.Err -} - -// NewContextualError creates a new contextual error with metadata -func NewContextualError(err error, component, operation string, metadata map[string]interface{}) *ContextualError { - return &ContextualError{ - Err: err, - Context: ErrorContext{ - Component: component, - Operation: operation, - Timestamp: time.Now(), - Metadata: metadata, - }, - } -} - -// WrapWithContext wraps an error with component and operation context -func WrapWithContext(err error, component, operation string) error { - if err == nil { - return nil - } - return NewContextualError(err, component, operation, nil) -} - -// WrapWithMetadata wraps an error with additional metadata -func WrapWithMetadata(err error, component, operation string, metadata map[string]interface{}) error { - if err == nil { - return nil - } - return NewContextualError(err, component, operation, metadata) -} - -// IsTimeoutError checks if an error is a timeout error -func IsTimeoutError(err error) bool { - return errors.Is(err, ErrOperationTimeout) -} - -// IsResourceError checks if an error is related to resource exhaustion -func IsResourceError(err error) bool { - return errors.Is(err, ErrResourceExhaustion) || errors.Is(err, ErrMemoryExhaustion) || errors.Is(err, ErrSystemOverload) -} - -// IsHardwareError checks if an error is hardware-related -func IsHardwareError(err error) bool { - return errors.Is(err, ErrHardwareFailure) -} - -// IsNetworkError checks if an error is network-related -func IsNetworkError(err error) bool { - return errors.Is(err, ErrNetworkError) -} - -// GetErrorSeverity returns the severity level of an error -func GetErrorSeverity(err error) string { - if IsHardwareError(err) { - return "critical" - } - if IsResourceError(err) { - return "high" - } - if IsNetworkError(err) || IsTimeoutError(err) { - return "medium" - } - return "low" -} diff --git a/internal/audio/validation_test.go b/internal/audio/validation_test.go new file mode 100644 index 0000000..ef936c0 --- /dev/null +++ b/internal/audio/validation_test.go @@ -0,0 +1,556 @@ +//go:build cgo +// +build cgo + +package audio + +import ( + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestValidationFunctions provides comprehensive testing of all validation functions +// to ensure they catch breaking changes and regressions effectively +func TestValidationFunctions(t *testing.T) { + tests := []struct { + name string + testFunc func(t *testing.T) + }{ + {"AudioQualityValidation", testAudioQualityValidation}, + {"FrameDataValidation", testFrameDataValidation}, + {"BufferSizeValidation", testBufferSizeValidation}, + {"ThreadPriorityValidation", testThreadPriorityValidation}, + {"LatencyValidation", testLatencyValidation}, + {"MetricsIntervalValidation", testMetricsIntervalValidation}, + {"SampleRateValidation", testSampleRateValidation}, + {"ChannelCountValidation", testChannelCountValidation}, + {"BitrateValidation", testBitrateValidation}, + {"FrameDurationValidation", testFrameDurationValidation}, + {"IPCConfigValidation", testIPCConfigValidation}, + {"AdaptiveBufferConfigValidation", testAdaptiveBufferConfigValidation}, + {"AudioConfigCompleteValidation", testAudioConfigCompleteValidation}, + {"ZeroCopyFrameValidation", testZeroCopyFrameValidation}, + {"AudioFrameFastValidation", testAudioFrameFastValidation}, + {"ErrorWrappingValidation", testErrorWrappingValidation}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.testFunc(t) + }) + } +} + +// testAudioQualityValidation tests audio quality validation with boundary conditions +func testAudioQualityValidation(t *testing.T) { + // Test valid quality levels + validQualities := []AudioQuality{AudioQualityLow, AudioQualityMedium, AudioQualityHigh, AudioQualityUltra} + for _, quality := range validQualities { + err := ValidateAudioQuality(quality) + assert.NoError(t, err, "Valid quality %d should pass validation", quality) + } + + // Test invalid quality levels + invalidQualities := []AudioQuality{-1, 4, 100, -100} + for _, quality := range invalidQualities { + err := ValidateAudioQuality(quality) + assert.Error(t, err, "Invalid quality %d should fail validation", quality) + assert.Contains(t, err.Error(), "invalid audio quality level", "Error should mention audio quality") + } +} + +// testFrameDataValidation tests frame data validation with various edge cases +func testFrameDataValidation(t *testing.T) { + config := GetConfig() + + // Test nil data + err := ValidateFrameData(nil) + assert.Error(t, err) + assert.Contains(t, err.Error(), "frame data is nil") + + // Test empty data + err = ValidateFrameData([]byte{}) + assert.Error(t, err) + assert.Contains(t, err.Error(), "frame data is empty") + + // Test data below minimum size + if config.MinFrameSize > 0 { + smallData := make([]byte, config.MinFrameSize-1) + err = ValidateFrameData(smallData) + assert.Error(t, err) + assert.Contains(t, err.Error(), "below minimum") + } + + // Test data above maximum size + largeData := make([]byte, config.MaxAudioFrameSize+1) + err = ValidateFrameData(largeData) + assert.Error(t, err) + assert.Contains(t, err.Error(), "exceeds maximum") + + // Test unaligned data (odd number of bytes for 16-bit samples) + oddData := make([]byte, 1001) // Odd number + if len(oddData) >= config.MinFrameSize { + err = ValidateFrameData(oddData) + assert.Error(t, err) + assert.Contains(t, err.Error(), "not aligned") + } + + // Test valid aligned data + validData := make([]byte, 1000) // Even number, within bounds + if len(validData) >= config.MinFrameSize && len(validData) <= config.MaxAudioFrameSize { + err = ValidateFrameData(validData) + assert.NoError(t, err) + } +} + +// testBufferSizeValidation tests buffer size validation +func testBufferSizeValidation(t *testing.T) { + config := GetConfig() + + // Test negative and zero sizes + invalidSizes := []int{-1, -100, 0} + for _, size := range invalidSizes { + err := ValidateBufferSize(size) + assert.Error(t, err, "Buffer size %d should be invalid", size) + assert.Contains(t, err.Error(), "must be positive") + } + + // Test size exceeding maximum + err := ValidateBufferSize(config.SocketMaxBuffer + 1) + assert.Error(t, err) + assert.Contains(t, err.Error(), "exceeds maximum") + + // Test valid sizes + validSizes := []int{1, 1024, 4096, config.SocketMaxBuffer} + for _, size := range validSizes { + err := ValidateBufferSize(size) + assert.NoError(t, err, "Buffer size %d should be valid", size) + } +} + +// testThreadPriorityValidation tests thread priority validation +func testThreadPriorityValidation(t *testing.T) { + // Test valid priorities + validPriorities := []int{-20, -10, 0, 10, 19} + for _, priority := range validPriorities { + err := ValidateThreadPriority(priority) + assert.NoError(t, err, "Priority %d should be valid", priority) + } + + // Test invalid priorities + invalidPriorities := []int{-21, -100, 20, 100} + for _, priority := range invalidPriorities { + err := ValidateThreadPriority(priority) + assert.Error(t, err, "Priority %d should be invalid", priority) + assert.Contains(t, err.Error(), "outside valid range") + } +} + +// testLatencyValidation tests latency validation +func testLatencyValidation(t *testing.T) { + config := GetConfig() + + // Test negative latency + err := ValidateLatency(-1 * time.Millisecond) + assert.Error(t, err) + assert.Contains(t, err.Error(), "cannot be negative") + + // Test zero latency (should be valid) + err = ValidateLatency(0) + assert.NoError(t, err) + + // Test very small positive latency + err = ValidateLatency(500 * time.Microsecond) + assert.Error(t, err) + assert.Contains(t, err.Error(), "below minimum") + + // Test latency exceeding maximum + err = ValidateLatency(config.MaxLatency + time.Second) + assert.Error(t, err) + assert.Contains(t, err.Error(), "exceeds maximum") + + // Test valid latencies + validLatencies := []time.Duration{ + 1 * time.Millisecond, + 10 * time.Millisecond, + 100 * time.Millisecond, + config.MaxLatency, + } + for _, latency := range validLatencies { + err := ValidateLatency(latency) + assert.NoError(t, err, "Latency %v should be valid", latency) + } +} + +// testMetricsIntervalValidation tests metrics interval validation +func testMetricsIntervalValidation(t *testing.T) { + config := GetConfig() + + // Test interval below minimum + err := ValidateMetricsInterval(config.MinMetricsUpdateInterval - time.Millisecond) + assert.Error(t, err) + + // Test interval above maximum + err = ValidateMetricsInterval(config.MaxMetricsUpdateInterval + time.Second) + assert.Error(t, err) + + // Test valid intervals + validIntervals := []time.Duration{ + config.MinMetricsUpdateInterval, + config.MaxMetricsUpdateInterval, + (config.MinMetricsUpdateInterval + config.MaxMetricsUpdateInterval) / 2, + } + for _, interval := range validIntervals { + err := ValidateMetricsInterval(interval) + assert.NoError(t, err, "Interval %v should be valid", interval) + } +} + +// testSampleRateValidation tests sample rate validation +func testSampleRateValidation(t *testing.T) { + config := GetConfig() + + // Test negative and zero sample rates + invalidRates := []int{-1, -48000, 0} + for _, rate := range invalidRates { + err := ValidateSampleRate(rate) + assert.Error(t, err, "Sample rate %d should be invalid", rate) + assert.Contains(t, err.Error(), "must be positive") + } + + // Test unsupported sample rates + unsupportedRates := []int{1000, 12345, 96001} + for _, rate := range unsupportedRates { + err := ValidateSampleRate(rate) + assert.Error(t, err, "Sample rate %d should be unsupported", rate) + assert.Contains(t, err.Error(), "not in supported rates") + } + + // Test valid sample rates + for _, rate := range config.ValidSampleRates { + err := ValidateSampleRate(rate) + assert.NoError(t, err, "Sample rate %d should be valid", rate) + } +} + +// testChannelCountValidation tests channel count validation +func testChannelCountValidation(t *testing.T) { + config := GetConfig() + + // Test invalid channel counts + invalidCounts := []int{-1, -10, 0} + for _, count := range invalidCounts { + err := ValidateChannelCount(count) + assert.Error(t, err, "Channel count %d should be invalid", count) + assert.Contains(t, err.Error(), "must be positive") + } + + // Test channel count exceeding maximum + err := ValidateChannelCount(config.MaxChannels + 1) + assert.Error(t, err) + assert.Contains(t, err.Error(), "exceeds maximum") + + // Test valid channel counts + validCounts := []int{1, 2, config.MaxChannels} + for _, count := range validCounts { + err := ValidateChannelCount(count) + assert.NoError(t, err, "Channel count %d should be valid", count) + } +} + +// testBitrateValidation tests bitrate validation +func testBitrateValidation(t *testing.T) { + // Test invalid bitrates + invalidBitrates := []int{-1, -1000, 0} + for _, bitrate := range invalidBitrates { + err := ValidateBitrate(bitrate) + assert.Error(t, err, "Bitrate %d should be invalid", bitrate) + assert.Contains(t, err.Error(), "must be positive") + } + + // Test bitrate below minimum (in kbps) + err := ValidateBitrate(5) // 5 kbps = 5000 bps < 6000 bps minimum + assert.Error(t, err) + assert.Contains(t, err.Error(), "below minimum") + + // Test bitrate above maximum (in kbps) + err = ValidateBitrate(511) // 511 kbps = 511000 bps > 510000 bps maximum + assert.Error(t, err) + assert.Contains(t, err.Error(), "exceeds maximum") + + // Test valid bitrates (in kbps) + validBitrates := []int{ + 6, // 6 kbps = 6000 bps (minimum) + 64, // Medium quality preset + 128, // High quality preset + 192, // Ultra quality preset + 510, // 510 kbps = 510000 bps (maximum) + } + for _, bitrate := range validBitrates { + err := ValidateBitrate(bitrate) + assert.NoError(t, err, "Bitrate %d kbps should be valid", bitrate) + } +} + +// testFrameDurationValidation tests frame duration validation +func testFrameDurationValidation(t *testing.T) { + config := GetConfig() + + // Test invalid durations + invalidDurations := []time.Duration{-1 * time.Millisecond, -1 * time.Second, 0} + for _, duration := range invalidDurations { + err := ValidateFrameDuration(duration) + assert.Error(t, err, "Duration %v should be invalid", duration) + assert.Contains(t, err.Error(), "must be positive") + } + + // Test duration below minimum + err := ValidateFrameDuration(config.MinFrameDuration - time.Microsecond) + assert.Error(t, err) + assert.Contains(t, err.Error(), "below minimum") + + // Test duration above maximum + err = ValidateFrameDuration(config.MaxFrameDuration + time.Second) + assert.Error(t, err) + assert.Contains(t, err.Error(), "exceeds maximum") + + // Test valid durations + validDurations := []time.Duration{ + config.MinFrameDuration, + config.MaxFrameDuration, + 20 * time.Millisecond, // Common frame duration + } + for _, duration := range validDurations { + err := ValidateFrameDuration(duration) + assert.NoError(t, err, "Duration %v should be valid", duration) + } +} + +// testIPCConfigValidation tests IPC configuration validation +func testIPCConfigValidation(t *testing.T) { + config := GetConfig() + + // Test invalid configurations for input IPC + invalidConfigs := []struct { + sampleRate, channels, frameSize int + description string + }{ + {0, 2, 960, "zero sample rate"}, + {48000, 0, 960, "zero channels"}, + {48000, 2, 0, "zero frame size"}, + {config.MinSampleRate - 1, 2, 960, "sample rate below minimum"}, + {config.MaxSampleRate + 1, 2, 960, "sample rate above maximum"}, + {48000, config.MaxChannels + 1, 960, "too many channels"}, + {48000, -1, 960, "negative channels"}, + {48000, 2, -1, "negative frame size"}, + } + + for _, tc := range invalidConfigs { + // Test input IPC validation + err := ValidateInputIPCConfig(tc.sampleRate, tc.channels, tc.frameSize) + assert.Error(t, err, "Input IPC config should be invalid: %s", tc.description) + + // Test output IPC validation + err = ValidateOutputIPCConfig(tc.sampleRate, tc.channels, tc.frameSize) + assert.Error(t, err, "Output IPC config should be invalid: %s", tc.description) + } + + // Test valid configuration + err := ValidateInputIPCConfig(48000, 2, 960) + assert.NoError(t, err) + err = ValidateOutputIPCConfig(48000, 2, 960) + assert.NoError(t, err) +} + +// testAdaptiveBufferConfigValidation tests adaptive buffer configuration validation +func testAdaptiveBufferConfigValidation(t *testing.T) { + config := GetConfig() + + // Test invalid configurations + invalidConfigs := []struct { + minSize, maxSize, defaultSize int + description string + }{ + {0, 1024, 512, "zero min size"}, + {-1, 1024, 512, "negative min size"}, + {512, 0, 256, "zero max size"}, + {512, -1, 256, "negative max size"}, + {512, 1024, 0, "zero default size"}, + {512, 1024, -1, "negative default size"}, + {1024, 512, 768, "min >= max"}, + {512, 1024, 256, "default < min"}, + {512, 1024, 2048, "default > max"}, + {512, config.SocketMaxBuffer + 1, 1024, "max exceeds global limit"}, + } + + for _, tc := range invalidConfigs { + err := ValidateAdaptiveBufferConfig(tc.minSize, tc.maxSize, tc.defaultSize) + assert.Error(t, err, "Config should be invalid: %s", tc.description) + } + + // Test valid configuration + err := ValidateAdaptiveBufferConfig(512, 4096, 1024) + assert.NoError(t, err) +} + +// testAudioConfigCompleteValidation tests complete audio configuration validation +func testAudioConfigCompleteValidation(t *testing.T) { + // Test valid configuration using actual preset values + validConfig := AudioConfig{ + Quality: AudioQualityMedium, + Bitrate: 64, // kbps - matches medium quality preset + SampleRate: 48000, + Channels: 2, + FrameSize: 20 * time.Millisecond, + } + err := ValidateAudioConfigComplete(validConfig) + assert.NoError(t, err) + + // Test invalid quality + invalidQualityConfig := validConfig + invalidQualityConfig.Quality = AudioQuality(99) + err = ValidateAudioConfigComplete(invalidQualityConfig) + assert.Error(t, err) + assert.Contains(t, err.Error(), "quality validation failed") + + // Test invalid bitrate + invalidBitrateConfig := validConfig + invalidBitrateConfig.Bitrate = -1 + err = ValidateAudioConfigComplete(invalidBitrateConfig) + assert.Error(t, err) + assert.Contains(t, err.Error(), "bitrate validation failed") + + // Test invalid sample rate + invalidSampleRateConfig := validConfig + invalidSampleRateConfig.SampleRate = 12345 + err = ValidateAudioConfigComplete(invalidSampleRateConfig) + assert.Error(t, err) + assert.Contains(t, err.Error(), "sample rate validation failed") + + // Test invalid channels + invalidChannelsConfig := validConfig + invalidChannelsConfig.Channels = 0 + err = ValidateAudioConfigComplete(invalidChannelsConfig) + assert.Error(t, err) + assert.Contains(t, err.Error(), "channel count validation failed") + + // Test invalid frame duration + invalidFrameDurationConfig := validConfig + invalidFrameDurationConfig.FrameSize = -1 * time.Millisecond + err = ValidateAudioConfigComplete(invalidFrameDurationConfig) + assert.Error(t, err) + assert.Contains(t, err.Error(), "frame duration validation failed") +} + +// testZeroCopyFrameValidation tests zero-copy frame validation +func testZeroCopyFrameValidation(t *testing.T) { + // Test nil frame + err := ValidateZeroCopyFrame(nil) + assert.Error(t, err) + + // Note: We can't easily test ZeroCopyAudioFrame without creating actual instances + // This would require more complex setup, but the validation logic is tested +} + +// testAudioFrameFastValidation tests fast audio frame validation +func testAudioFrameFastValidation(t *testing.T) { + config := GetConfig() + + // Test empty data + err := ValidateAudioFrameFast([]byte{}) + assert.Error(t, err) + assert.Contains(t, err.Error(), "frame data is empty") + + // Test data exceeding maximum size + largeData := make([]byte, config.MaxAudioFrameSize+1) + err = ValidateAudioFrameFast(largeData) + assert.Error(t, err) + assert.Contains(t, err.Error(), "exceeds maximum") + + // Test valid data + validData := make([]byte, 1000) + err = ValidateAudioFrameFast(validData) + assert.NoError(t, err) +} + +// testErrorWrappingValidation tests error wrapping functionality +func testErrorWrappingValidation(t *testing.T) { + // Test wrapping nil error + wrapped := WrapWithMetadata(nil, "component", "operation", map[string]interface{}{"key": "value"}) + assert.Nil(t, wrapped) + + // Test wrapping actual error + originalErr := assert.AnError + metadata := map[string]interface{}{ + "frame_size": 1024, + "quality": "high", + } + wrapped = WrapWithMetadata(originalErr, "audio", "decode", metadata) + require.NotNil(t, wrapped) + assert.Contains(t, wrapped.Error(), "audio.decode") + assert.Contains(t, wrapped.Error(), "assert.AnError") + assert.Contains(t, wrapped.Error(), "metadata") + assert.Contains(t, wrapped.Error(), "frame_size") + assert.Contains(t, wrapped.Error(), "quality") +} + +// TestValidationIntegration tests validation functions working together +func TestValidationIntegration(t *testing.T) { + // Test that validation functions work correctly with actual audio configurations + presets := GetAudioQualityPresets() + require.NotEmpty(t, presets) + + for quality, config := range presets { + t.Run(fmt.Sprintf("Quality_%d", quality), func(t *testing.T) { + // Validate the preset configuration + err := ValidateAudioConfigComplete(config) + assert.NoError(t, err, "Preset configuration for quality %d should be valid", quality) + + // Validate individual components + err = ValidateAudioQuality(config.Quality) + assert.NoError(t, err, "Quality should be valid") + + err = ValidateBitrate(config.Bitrate) + assert.NoError(t, err, "Bitrate should be valid") + + err = ValidateSampleRate(config.SampleRate) + assert.NoError(t, err, "Sample rate should be valid") + + err = ValidateChannelCount(config.Channels) + assert.NoError(t, err, "Channel count should be valid") + + err = ValidateFrameDuration(config.FrameSize) + assert.NoError(t, err, "Frame duration should be valid") + }) + } +} + +// TestValidationPerformance ensures validation functions are efficient +func TestValidationPerformance(t *testing.T) { + if testing.Short() { + t.Skip("Skipping performance test in short mode") + } + + // Test that validation functions complete quickly + start := time.Now() + iterations := 10000 + + for i := 0; i < iterations; i++ { + _ = ValidateAudioQuality(AudioQualityMedium) + _ = ValidateBufferSize(1024) + _ = ValidateChannelCount(2) + _ = ValidateSampleRate(48000) + _ = ValidateBitrate(96) // 96 kbps + } + + elapsed := time.Since(start) + perIteration := elapsed / time.Duration(iterations) + + // Performance expectations for JetKVM (ARM Cortex-A7 @ 1GHz, 256MB RAM) + // Audio processing must not interfere with primary KVM functionality + assert.Less(t, perIteration, 200*time.Microsecond, "Validation should not impact KVM performance") + t.Logf("Validation performance: %v per iteration", perIteration) +} \ No newline at end of file