Go-based FM stereo transmitter with RDS, Windows-first and cross-platform
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

121 lines
3.1KB

  1. package ingest
  2. import (
  3. "context"
  4. "errors"
  5. "sync"
  6. "testing"
  7. "time"
  8. "github.com/jan/fm-rds-tx/internal/audio"
  9. )
  10. type fakeSource struct {
  11. desc SourceDescriptor
  12. chunks chan PCMChunk
  13. errs chan error
  14. stats SourceStats
  15. once sync.Once
  16. }
  17. func newFakeSource() *fakeSource {
  18. return &fakeSource{
  19. desc: SourceDescriptor{ID: "fake", Kind: "stdin-pcm"},
  20. chunks: make(chan PCMChunk, 4),
  21. errs: make(chan error, 1),
  22. stats: SourceStats{State: "running", Connected: true},
  23. }
  24. }
  25. func (s *fakeSource) Descriptor() SourceDescriptor { return s.desc }
  26. func (s *fakeSource) Start(context.Context) error { return nil }
  27. func (s *fakeSource) Stop() error { s.once.Do(func() { close(s.chunks) }); return nil }
  28. func (s *fakeSource) Chunks() <-chan PCMChunk { return s.chunks }
  29. func (s *fakeSource) Errors() <-chan error { return s.errs }
  30. func (s *fakeSource) Stats() SourceStats { return s.stats }
  31. func TestRuntimeWritesFramesToStreamSink(t *testing.T) {
  32. sink := audio.NewStreamSource(128, 44100)
  33. src := newFakeSource()
  34. rt := NewRuntime(sink, src)
  35. if err := rt.Start(context.Background()); err != nil {
  36. t.Fatalf("start: %v", err)
  37. }
  38. defer rt.Stop()
  39. src.chunks <- PCMChunk{
  40. Channels: 2,
  41. SampleRateHz: 44100,
  42. Samples: []int32{1000 << 16, -1000 << 16},
  43. }
  44. deadline := time.Now().Add(1 * time.Second)
  45. for sink.Available() < 1 && time.Now().Before(deadline) {
  46. time.Sleep(10 * time.Millisecond)
  47. }
  48. if sink.Available() < 1 {
  49. t.Fatal("expected at least one frame in sink")
  50. }
  51. }
  52. func TestRuntimeRecoversToRunningAfterSourceError(t *testing.T) {
  53. sink := audio.NewStreamSource(128, 44100)
  54. src := newFakeSource()
  55. rt := NewRuntime(sink, src)
  56. if err := rt.Start(context.Background()); err != nil {
  57. t.Fatalf("start: %v", err)
  58. }
  59. defer rt.Stop()
  60. src.errs <- errors.New("decode transient failure")
  61. waitForRuntimeState(t, rt, "degraded")
  62. src.chunks <- PCMChunk{
  63. Channels: 2,
  64. SampleRateHz: 44100,
  65. Samples: []int32{500 << 16, -500 << 16},
  66. }
  67. waitForRuntimeState(t, rt, "running")
  68. }
  69. func TestRuntimeRecoversToRunningAfterConvertError(t *testing.T) {
  70. sink := audio.NewStreamSource(128, 44100)
  71. src := newFakeSource()
  72. rt := NewRuntime(sink, src)
  73. if err := rt.Start(context.Background()); err != nil {
  74. t.Fatalf("start: %v", err)
  75. }
  76. defer rt.Stop()
  77. // Invalid stereo chunk: odd sample count causes conversion error.
  78. src.chunks <- PCMChunk{
  79. Channels: 2,
  80. SampleRateHz: 44100,
  81. Samples: []int32{100 << 16},
  82. }
  83. waitForRuntimeState(t, rt, "degraded")
  84. if got := rt.Stats().Runtime.ConvertErrors; got != 1 {
  85. t.Fatalf("convertErrors=%d want 1", got)
  86. }
  87. src.chunks <- PCMChunk{
  88. Channels: 2,
  89. SampleRateHz: 44100,
  90. Samples: []int32{300 << 16, -300 << 16},
  91. }
  92. waitForRuntimeState(t, rt, "running")
  93. }
  94. func waitForRuntimeState(t *testing.T, rt *Runtime, want string) {
  95. t.Helper()
  96. deadline := time.Now().Add(1 * time.Second)
  97. for time.Now().Before(deadline) {
  98. if got := rt.Stats().Runtime.State; got == want {
  99. return
  100. }
  101. time.Sleep(10 * time.Millisecond)
  102. }
  103. t.Fatalf("timeout waiting for runtime state %q; last=%q", want, rt.Stats().Runtime.State)
  104. }