package ingest import ( "context" "errors" "sync" "testing" "time" "github.com/jan/fm-rds-tx/internal/audio" ) type fakeSource struct { desc SourceDescriptor chunks chan PCMChunk errs chan error stats SourceStats once sync.Once } func newFakeSource() *fakeSource { return &fakeSource{ desc: SourceDescriptor{ID: "fake", Kind: "stdin-pcm"}, chunks: make(chan PCMChunk, 4), errs: make(chan error, 1), stats: SourceStats{State: "running", Connected: true}, } } func (s *fakeSource) Descriptor() SourceDescriptor { return s.desc } func (s *fakeSource) Start(context.Context) error { return nil } func (s *fakeSource) Stop() error { s.once.Do(func() { close(s.chunks) }); return nil } func (s *fakeSource) Chunks() <-chan PCMChunk { return s.chunks } func (s *fakeSource) Errors() <-chan error { return s.errs } func (s *fakeSource) Stats() SourceStats { return s.stats } func TestRuntimeWritesFramesToStreamSink(t *testing.T) { sink := audio.NewStreamSource(128, 44100) src := newFakeSource() rt := NewRuntime(sink, src) if err := rt.Start(context.Background()); err != nil { t.Fatalf("start: %v", err) } defer rt.Stop() src.chunks <- PCMChunk{ Channels: 2, SampleRateHz: 44100, Samples: []int32{1000 << 16, -1000 << 16}, } deadline := time.Now().Add(1 * time.Second) for sink.Available() < 1 && time.Now().Before(deadline) { time.Sleep(10 * time.Millisecond) } if sink.Available() < 1 { t.Fatal("expected at least one frame in sink") } } func TestRuntimeRecoversToRunningAfterSourceError(t *testing.T) { sink := audio.NewStreamSource(128, 44100) src := newFakeSource() rt := NewRuntime(sink, src) if err := rt.Start(context.Background()); err != nil { t.Fatalf("start: %v", err) } defer rt.Stop() src.errs <- errors.New("decode transient failure") waitForRuntimeState(t, rt, "degraded") src.chunks <- PCMChunk{ Channels: 2, SampleRateHz: 44100, Samples: []int32{500 << 16, -500 << 16}, } waitForRuntimeState(t, rt, "running") } func TestRuntimeRecoversToRunningAfterConvertError(t *testing.T) { sink := audio.NewStreamSource(128, 44100) src := newFakeSource() rt := NewRuntime(sink, src) if err := rt.Start(context.Background()); err != nil { t.Fatalf("start: %v", err) } defer rt.Stop() // Invalid stereo chunk: odd sample count causes conversion error. src.chunks <- PCMChunk{ Channels: 2, SampleRateHz: 44100, Samples: []int32{100 << 16}, } waitForRuntimeState(t, rt, "degraded") if got := rt.Stats().Runtime.ConvertErrors; got != 1 { t.Fatalf("convertErrors=%d want 1", got) } src.chunks <- PCMChunk{ Channels: 2, SampleRateHz: 44100, Samples: []int32{300 << 16, -300 << 16}, } waitForRuntimeState(t, rt, "running") } func waitForRuntimeState(t *testing.T, rt *Runtime, want string) { t.Helper() deadline := time.Now().Add(1 * time.Second) for time.Now().Before(deadline) { if got := rt.Stats().Runtime.State; got == want { return } time.Sleep(10 * time.Millisecond) } t.Fatalf("timeout waiting for runtime state %q; last=%q", want, rt.Stats().Runtime.State) }