package app import ( "context" "testing" "time" cfgpkg "github.com/jan/fm-rds-tx/internal/config" "github.com/jan/fm-rds-tx/internal/platform" ) func TestEngineContinuousRun(t *testing.T) { cfg := cfgpkg.Default() driver := platform.NewSimulatedDriver(nil) eng := NewEngine(cfg, driver) eng.SetChunkDuration(10 * time.Millisecond) ctx := context.Background() if err := eng.Start(ctx); err != nil { t.Fatalf("start: %v", err) } // Let it run for 200ms time.Sleep(200 * time.Millisecond) stats := eng.Stats() if stats.State != "running" { t.Fatalf("expected running, got %s", stats.State) } if stats.ChunksProduced < 5 { t.Fatalf("expected at least 5 chunks, got %d", stats.ChunksProduced) } if stats.TotalSamples == 0 { t.Fatal("expected non-zero samples") } if err := eng.Stop(ctx); err != nil { t.Fatalf("stop: %v", err) } stats = eng.Stats() if stats.State != "idle" { t.Fatalf("expected idle after stop, got %s", stats.State) } } func TestEngineDoubleStartFails(t *testing.T) { cfg := cfgpkg.Default() driver := platform.NewSimulatedDriver(nil) eng := NewEngine(cfg, driver) ctx := context.Background() if err := eng.Start(ctx); err != nil { t.Fatalf("first start: %v", err) } defer eng.Stop(ctx) if err := eng.Start(ctx); err == nil { t.Fatal("expected error on double start") } } func TestEngineDriverStats(t *testing.T) { cfg := cfgpkg.Default() driver := platform.NewSimulatedDriver(nil) eng := NewEngine(cfg, driver) eng.SetChunkDuration(10 * time.Millisecond) ctx := context.Background() _ = eng.Start(ctx) time.Sleep(100 * time.Millisecond) _ = eng.Stop(ctx) driverStats := driver.Stats() if driverStats.SamplesWritten == 0 { t.Fatal("expected driver to have written samples") } if driverStats.FramesWritten == 0 { t.Fatal("expected driver to have written frames") } } func TestEngineSplitRate(t *testing.T) { // Simulate Pluto-like config: composite at 228kHz, device at 2.28MHz cfg := cfgpkg.Default() cfg.Backend.DeviceSampleRateHz = 2280000 // 10:1 ratio driver := platform.NewSimulatedDriver(nil) eng := NewEngine(cfg, driver) eng.SetChunkDuration(10 * time.Millisecond) // Verify split-rate path was chosen if eng.upsampler == nil { t.Fatal("expected split-rate mode (upsampler != nil)") } ctx := context.Background() if err := eng.Start(ctx); err != nil { t.Fatalf("start: %v", err) } time.Sleep(200 * time.Millisecond) stats := eng.Stats() if stats.ChunksProduced < 5 { t.Fatalf("expected at least 5 chunks, got %d", stats.ChunksProduced) } // With 10:1 upsampling and 10ms chunks at 228kHz source: // 2280 src samples → 22800 output samples per chunk // 5 chunks minimum → at least 114000 samples if stats.TotalSamples < 50000 { t.Fatalf("expected at least 50000 upsampled samples, got %d", stats.TotalSamples) } if err := eng.Stop(ctx); err != nil { t.Fatalf("stop: %v", err) } if stats.Underruns > 0 { t.Fatalf("unexpected underruns: %d", stats.Underruns) } } func TestEngineSameRate(t *testing.T) { // Default config: deviceSampleRateHz=0, compositeRateHz=228000 // EffectiveDeviceRate = 228000 → same-rate mode, no upsampler cfg := cfgpkg.Default() driver := platform.NewSimulatedDriver(nil) eng := NewEngine(cfg, driver) if eng.upsampler != nil { t.Fatal("expected same-rate mode (upsampler == nil)") } } func TestEngineLiveUpdateDSP(t *testing.T) { cfg := cfgpkg.Default() driver := platform.NewSimulatedDriver(nil) eng := NewEngine(cfg, driver) eng.SetChunkDuration(10 * time.Millisecond) ctx := context.Background() if err := eng.Start(ctx); err != nil { t.Fatalf("start: %v", err) } defer eng.Stop(ctx) time.Sleep(50 * time.Millisecond) // Update DSP params while running drive := 1.5 stereo := false err := eng.UpdateConfig(LiveConfigUpdate{ OutputDrive: &drive, StereoEnabled: &stereo, }) if err != nil { t.Fatalf("UpdateConfig: %v", err) } // Engine should still be running after update time.Sleep(50 * time.Millisecond) stats := eng.Stats() if stats.State != "running" { t.Fatalf("expected running after update, got %s", stats.State) } if stats.Underruns > 0 { t.Fatalf("unexpected underruns after update: %d", stats.Underruns) } } func TestEngineLiveUpdateFrequency(t *testing.T) { cfg := cfgpkg.Default() driver := platform.NewSimulatedDriver(nil) eng := NewEngine(cfg, driver) eng.SetChunkDuration(10 * time.Millisecond) ctx := context.Background() if err := eng.Start(ctx); err != nil { t.Fatalf("start: %v", err) } defer eng.Stop(ctx) time.Sleep(50 * time.Millisecond) // Tune frequency freq := 99.5 err := eng.UpdateConfig(LiveConfigUpdate{FrequencyMHz: &freq}) if err != nil { t.Fatalf("UpdateConfig freq: %v", err) } // Let it process for a bit so the pending freq gets applied time.Sleep(50 * time.Millisecond) stats := eng.Stats() if stats.State != "running" { t.Fatalf("expected running after tune, got %s", stats.State) } } func TestEngineLiveUpdateRDS(t *testing.T) { cfg := cfgpkg.Default() driver := platform.NewSimulatedDriver(nil) eng := NewEngine(cfg, driver) eng.SetChunkDuration(10 * time.Millisecond) ctx := context.Background() if err := eng.Start(ctx); err != nil { t.Fatalf("start: %v", err) } defer eng.Stop(ctx) time.Sleep(50 * time.Millisecond) // Update RDS text ps := "NEWPS" rt := "Now playing: test track" err := eng.UpdateConfig(LiveConfigUpdate{PS: &ps, RadioText: &rt}) if err != nil { t.Fatalf("UpdateConfig RDS: %v", err) } time.Sleep(50 * time.Millisecond) stats := eng.Stats() if stats.Underruns > 0 { t.Fatalf("underruns after RDS update: %d", stats.Underruns) } } func TestEngineLiveUpdateValidation(t *testing.T) { cfg := cfgpkg.Default() driver := platform.NewSimulatedDriver(nil) eng := NewEngine(cfg, driver) // Out of range frequency badFreq := 200.0 if err := eng.UpdateConfig(LiveConfigUpdate{FrequencyMHz: &badFreq}); err == nil { t.Fatal("expected validation error for bad frequency") } // Out of range drive badDrive := 11.0 if err := eng.UpdateConfig(LiveConfigUpdate{OutputDrive: &badDrive}); err == nil { t.Fatal("expected validation error for bad drive") } // Valid update should succeed goodDrive := 1.0 if err := eng.UpdateConfig(LiveConfigUpdate{OutputDrive: &goodDrive}); err != nil { t.Fatalf("expected valid update to succeed: %v", err) } }