Go-based FM stereo transmitter with RDS, Windows-first and cross-platform
您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

202 行
5.4KB

  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. title chan string
  15. stats SourceStats
  16. once sync.Once
  17. }
  18. func newFakeSource() *fakeSource {
  19. return &fakeSource{
  20. desc: SourceDescriptor{ID: "fake", Kind: "stdin-pcm"},
  21. chunks: make(chan PCMChunk, 4),
  22. errs: make(chan error, 1),
  23. title: make(chan string, 4),
  24. stats: SourceStats{State: "running", Connected: true},
  25. }
  26. }
  27. func (s *fakeSource) Descriptor() SourceDescriptor { return s.desc }
  28. func (s *fakeSource) Start(context.Context) error { return nil }
  29. func (s *fakeSource) Stop() error { s.once.Do(func() { close(s.chunks) }); return nil }
  30. func (s *fakeSource) Chunks() <-chan PCMChunk { return s.chunks }
  31. func (s *fakeSource) Errors() <-chan error { return s.errs }
  32. func (s *fakeSource) StreamTitleUpdates() <-chan string {
  33. return s.title
  34. }
  35. func (s *fakeSource) Stats() SourceStats { return s.stats }
  36. func TestRuntimeWritesFramesToStreamSink(t *testing.T) {
  37. sink := audio.NewStreamSource(128, 44100)
  38. src := newFakeSource()
  39. rt := NewRuntime(sink, src)
  40. if err := rt.Start(context.Background()); err != nil {
  41. t.Fatalf("start: %v", err)
  42. }
  43. defer rt.Stop()
  44. src.chunks <- PCMChunk{
  45. Channels: 2,
  46. SampleRateHz: 44100,
  47. Samples: []int32{1000 << 16, -1000 << 16},
  48. }
  49. deadline := time.Now().Add(1 * time.Second)
  50. for sink.Available() < 1 && time.Now().Before(deadline) {
  51. time.Sleep(10 * time.Millisecond)
  52. }
  53. if sink.Available() < 1 {
  54. t.Fatal("expected at least one frame in sink")
  55. }
  56. }
  57. func TestRuntimeRecoversToRunningAfterSourceError(t *testing.T) {
  58. sink := audio.NewStreamSource(128, 44100)
  59. src := newFakeSource()
  60. rt := NewRuntime(sink, src)
  61. if err := rt.Start(context.Background()); err != nil {
  62. t.Fatalf("start: %v", err)
  63. }
  64. defer rt.Stop()
  65. src.errs <- errors.New("decode transient failure")
  66. waitForRuntimeState(t, rt, "degraded")
  67. src.chunks <- PCMChunk{
  68. Channels: 2,
  69. SampleRateHz: 44100,
  70. Samples: []int32{500 << 16, -500 << 16},
  71. }
  72. waitForRuntimeState(t, rt, "running")
  73. }
  74. func TestRuntimeRecoversToRunningAfterConvertError(t *testing.T) {
  75. sink := audio.NewStreamSource(128, 44100)
  76. src := newFakeSource()
  77. rt := NewRuntime(sink, src)
  78. if err := rt.Start(context.Background()); err != nil {
  79. t.Fatalf("start: %v", err)
  80. }
  81. defer rt.Stop()
  82. // Invalid stereo chunk: odd sample count causes conversion error.
  83. src.chunks <- PCMChunk{
  84. Channels: 2,
  85. SampleRateHz: 44100,
  86. Samples: []int32{100 << 16},
  87. }
  88. waitForRuntimeState(t, rt, "degraded")
  89. if got := rt.Stats().Runtime.ConvertErrors; got != 1 {
  90. t.Fatalf("convertErrors=%d want 1", got)
  91. }
  92. src.chunks <- PCMChunk{
  93. Channels: 2,
  94. SampleRateHz: 44100,
  95. Samples: []int32{300 << 16, -300 << 16},
  96. }
  97. waitForRuntimeState(t, rt, "running")
  98. }
  99. func TestRuntimeWithMissingSourceStaysIdleAndReturnsZeroSourceStats(t *testing.T) {
  100. sink := audio.NewStreamSource(128, 44100)
  101. rt := NewRuntime(sink, nil)
  102. if err := rt.Start(context.Background()); err != nil {
  103. t.Fatalf("start: %v", err)
  104. }
  105. stats := rt.Stats()
  106. if stats.Runtime.State != "idle" {
  107. t.Fatalf("runtime state=%q want idle", stats.Runtime.State)
  108. }
  109. if stats.Active.ID != "" || stats.Active.Kind != "" {
  110. t.Fatalf("expected empty active descriptor, got %+v", stats.Active)
  111. }
  112. if stats.Source.State != "" {
  113. t.Fatalf("expected zero-value source stats, got state=%q", stats.Source.State)
  114. }
  115. }
  116. func TestRuntimeStatsExposeActiveDescriptorAndSourceReconnectState(t *testing.T) {
  117. sink := audio.NewStreamSource(128, 44100)
  118. src := newFakeSource()
  119. src.desc = SourceDescriptor{ID: "icecast-primary", Kind: "icecast"}
  120. src.stats = SourceStats{
  121. State: "reconnecting",
  122. Connected: false,
  123. Reconnects: 4,
  124. LastError: "stream ended",
  125. }
  126. rt := NewRuntime(sink, src)
  127. if err := rt.Start(context.Background()); err != nil {
  128. t.Fatalf("start: %v", err)
  129. }
  130. defer rt.Stop()
  131. waitForRuntimeState(t, rt, "running")
  132. stats := rt.Stats()
  133. if stats.Active.ID != "icecast-primary" {
  134. t.Fatalf("active id=%q want icecast-primary", stats.Active.ID)
  135. }
  136. if stats.Active.Kind != "icecast" {
  137. t.Fatalf("active kind=%q want icecast", stats.Active.Kind)
  138. }
  139. if stats.Source.Reconnects != 4 {
  140. t.Fatalf("source reconnects=%d want 4", stats.Source.Reconnects)
  141. }
  142. if stats.Source.LastError != "stream ended" {
  143. t.Fatalf("source lastError=%q want stream ended", stats.Source.LastError)
  144. }
  145. }
  146. func TestRuntimeForwardsStreamTitleUpdatesToHandler(t *testing.T) {
  147. sink := audio.NewStreamSource(128, 44100)
  148. src := newFakeSource()
  149. got := make(chan string, 1)
  150. rt := NewRuntime(sink, src, WithStreamTitleHandler(func(title string) {
  151. got <- title
  152. }))
  153. if err := rt.Start(context.Background()); err != nil {
  154. t.Fatalf("start: %v", err)
  155. }
  156. defer rt.Stop()
  157. src.title <- "Artist - Song"
  158. select {
  159. case title := <-got:
  160. if title != "Artist - Song" {
  161. t.Fatalf("title=%q want %q", title, "Artist - Song")
  162. }
  163. case <-time.After(1 * time.Second):
  164. t.Fatal("timed out waiting for forwarded title")
  165. }
  166. }
  167. func waitForRuntimeState(t *testing.T, rt *Runtime, want string) {
  168. t.Helper()
  169. deadline := time.Now().Add(1 * time.Second)
  170. for time.Now().Before(deadline) {
  171. if got := rt.Stats().Runtime.State; got == want {
  172. return
  173. }
  174. time.Sleep(10 * time.Millisecond)
  175. }
  176. t.Fatalf("timeout waiting for runtime state %q; last=%q", want, rt.Stats().Runtime.State)
  177. }