Go-based FM stereo transmitter with RDS, Windows-first and cross-platform
Du kannst nicht mehr als 25 Themen auswählen Themen müssen entweder mit einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.

319 Zeilen
8.0KB

  1. package config
  2. import (
  3. "os"
  4. "path/filepath"
  5. "strings"
  6. "testing"
  7. )
  8. func TestDefaultValidate(t *testing.T) {
  9. if err := Default().Validate(); err != nil {
  10. t.Fatalf("default invalid: %v", err)
  11. }
  12. }
  13. func TestLoadAndValidate(t *testing.T) {
  14. dir := t.TempDir()
  15. path := filepath.Join(dir, "config.json")
  16. os.WriteFile(path, []byte(`{"audio":{"toneLeftHz":900,"toneRightHz":1700,"toneAmplitude":0.3},"fm":{"frequencyMHz":99.9},"backend":{"kind":"file","outputPath":"out.f32"},"control":{"listenAddress":"127.0.0.1:8088"}}`), 0o644)
  17. cfg, err := Load(path)
  18. if err != nil {
  19. t.Fatalf("load: %v", err)
  20. }
  21. if cfg.Audio.ToneLeftHz != 900 {
  22. t.Fatalf("unexpected left tone: %v", cfg.Audio.ToneLeftHz)
  23. }
  24. }
  25. func TestValidateRejectsBadFrequency(t *testing.T) {
  26. cfg := Default()
  27. cfg.FM.FrequencyMHz = 200
  28. if err := cfg.Validate(); err == nil {
  29. t.Fatal("expected error")
  30. }
  31. }
  32. func TestValidateRejectsBadPreEmphasis(t *testing.T) {
  33. cfg := Default()
  34. cfg.FM.PreEmphasisTauUS = 150
  35. if err := cfg.Validate(); err == nil {
  36. t.Fatal("expected error")
  37. }
  38. }
  39. func TestDefaultPreEmphasis(t *testing.T) {
  40. if Default().FM.PreEmphasisTauUS != 50 {
  41. t.Fatal("expected 50")
  42. }
  43. }
  44. func TestDefaultFMModulation(t *testing.T) {
  45. cfg := Default()
  46. if !cfg.FM.FMModulationEnabled {
  47. t.Fatal("expected true")
  48. }
  49. if cfg.FM.MaxDeviationHz != 75000 {
  50. t.Fatal("expected 75000")
  51. }
  52. }
  53. func TestParsePI(t *testing.T) {
  54. tests := []struct {
  55. in string
  56. want uint16
  57. ok bool
  58. }{
  59. {"1234", 0x1234, true}, {"0xBEEF", 0xBEEF, true}, {"0XCAFE", 0xCAFE, true},
  60. {" 0x2345 ", 0x2345, true}, {"", 0, false}, {"nope", 0, false},
  61. }
  62. for _, tt := range tests {
  63. got, err := ParsePI(tt.in)
  64. if tt.ok && err != nil {
  65. t.Fatalf("ParsePI(%q): %v", tt.in, err)
  66. }
  67. if !tt.ok && err == nil {
  68. t.Fatalf("ParsePI(%q): expected error", tt.in)
  69. }
  70. if tt.ok && got != tt.want {
  71. t.Fatalf("ParsePI(%q): got %x want %x", tt.in, got, tt.want)
  72. }
  73. }
  74. }
  75. func TestValidateRejectsInvalidPI(t *testing.T) {
  76. cfg := Default()
  77. cfg.RDS.PI = "nope"
  78. if err := cfg.Validate(); err == nil {
  79. t.Fatal("expected error")
  80. }
  81. }
  82. func TestValidateRejectsEmptyPI(t *testing.T) {
  83. cfg := Default()
  84. cfg.RDS.PI = ""
  85. if err := cfg.Validate(); err == nil {
  86. t.Fatal("expected error")
  87. }
  88. }
  89. func TestValidateRejectsLongPS(t *testing.T) {
  90. cfg := Default()
  91. cfg.RDS.PS = "TOO_LONG_PS"
  92. if err := cfg.Validate(); err == nil {
  93. t.Fatal("expected error for PS longer than 8 characters")
  94. }
  95. }
  96. func TestValidateRejectsLongRadioText(t *testing.T) {
  97. cfg := Default()
  98. cfg.RDS.RadioText = strings.Repeat("x", 65)
  99. if err := cfg.Validate(); err == nil {
  100. t.Fatal("expected error for RadioText longer than 64 characters")
  101. }
  102. }
  103. func TestEffectiveDeviceRate(t *testing.T) {
  104. cfg := Default()
  105. if cfg.EffectiveDeviceRate() != float64(cfg.FM.CompositeRateHz) {
  106. t.Fatal("expected composite rate")
  107. }
  108. cfg.Backend.DeviceSampleRateHz = 912000
  109. if cfg.EffectiveDeviceRate() != 912000 {
  110. t.Fatal("expected 912000")
  111. }
  112. }
  113. func TestValidateRejectsUnsupportedIngestKind(t *testing.T) {
  114. cfg := Default()
  115. cfg.Ingest.Kind = "unsupported"
  116. if err := cfg.Validate(); err == nil {
  117. t.Fatal("expected error")
  118. }
  119. }
  120. func TestValidateRejectsInvalidSRTConfig(t *testing.T) {
  121. cfg := Default()
  122. cfg.Ingest.Kind = "srt"
  123. cfg.Ingest.SRT.URL = ""
  124. if err := cfg.Validate(); err == nil {
  125. t.Fatal("expected srt url error")
  126. }
  127. cfg = Default()
  128. cfg.Ingest.Kind = "srt"
  129. cfg.Ingest.SRT.URL = "srt://127.0.0.1:9000"
  130. cfg.Ingest.SRT.Mode = "invalid"
  131. if err := cfg.Validate(); err == nil {
  132. t.Fatal("expected srt mode error")
  133. }
  134. cfg = Default()
  135. cfg.Ingest.Kind = "srt"
  136. cfg.Ingest.SRT.URL = "srt://127.0.0.1:9000"
  137. cfg.Ingest.SRT.SampleRateHz = 0
  138. if err := cfg.Validate(); err == nil {
  139. t.Fatal("expected srt sample rate error")
  140. }
  141. cfg = Default()
  142. cfg.Ingest.Kind = "srt"
  143. cfg.Ingest.SRT.URL = "srt://127.0.0.1:9000"
  144. cfg.Ingest.SRT.Channels = 3
  145. if err := cfg.Validate(); err == nil {
  146. t.Fatal("expected srt channels error")
  147. }
  148. }
  149. func TestValidateRejectsInvalidAES67Config(t *testing.T) {
  150. cfg := Default()
  151. cfg.Ingest.Kind = "aes67"
  152. cfg.Ingest.AES67.MulticastGroup = ""
  153. if err := cfg.Validate(); err == nil {
  154. t.Fatal("expected aes67 multicast group error")
  155. }
  156. cfg = Default()
  157. cfg.Ingest.Kind = "aes67"
  158. cfg.Ingest.AES67.MulticastGroup = "239.10.20.30"
  159. cfg.Ingest.AES67.Port = 5004
  160. cfg.Ingest.AES67.Encoding = "L16"
  161. if err := cfg.Validate(); err == nil {
  162. t.Fatal("expected aes67 encoding error")
  163. }
  164. cfg = Default()
  165. cfg.Ingest.Kind = "aes67"
  166. cfg.Ingest.AES67.MulticastGroup = "239.10.20.30"
  167. cfg.Ingest.AES67.Port = 5004
  168. cfg.Ingest.AES67.SDP = "v=0"
  169. cfg.Ingest.AES67.SDPPath = "stream.sdp"
  170. if err := cfg.Validate(); err == nil {
  171. t.Fatal("expected mutually exclusive sdp/sdpPath error")
  172. }
  173. }
  174. func TestValidateAcceptsAES67WithSDPOnly(t *testing.T) {
  175. cfg := Default()
  176. cfg.Ingest.Kind = "aes67"
  177. cfg.Ingest.AES67.MulticastGroup = ""
  178. cfg.Ingest.AES67.SDP = "v=0\r\ns=demo\r\nc=IN IP4 239.10.20.30\r\nm=audio 5004 RTP/AVP 97\r\na=rtpmap:97 L24/48000/2\r\n"
  179. if err := cfg.Validate(); err != nil {
  180. t.Fatalf("expected aes67 with SDP to validate: %v", err)
  181. }
  182. }
  183. func TestValidateAcceptsAES67WithDiscoveryOnly(t *testing.T) {
  184. cfg := Default()
  185. cfg.Ingest.Kind = "aes67"
  186. cfg.Ingest.AES67.MulticastGroup = ""
  187. cfg.Ingest.AES67.Port = 0
  188. cfg.Ingest.AES67.Discovery.StreamName = "AES67-MAIN"
  189. if err := cfg.Validate(); err != nil {
  190. t.Fatalf("expected aes67 discovery config to validate: %v", err)
  191. }
  192. }
  193. func TestValidateRejectsAES67DiscoveryWithoutStreamName(t *testing.T) {
  194. cfg := Default()
  195. cfg.Ingest.Kind = "aes67"
  196. cfg.Ingest.AES67.MulticastGroup = ""
  197. cfg.Ingest.AES67.Port = 0
  198. cfg.Ingest.AES67.Discovery.Enabled = true
  199. cfg.Ingest.AES67.Discovery.StreamName = ""
  200. if err := cfg.Validate(); err == nil {
  201. t.Fatal("expected discovery streamName validation error")
  202. }
  203. }
  204. func TestValidateRejectsAES67DiscoverySAPPortOutOfRange(t *testing.T) {
  205. cfg := Default()
  206. cfg.Ingest.Kind = "aes67"
  207. cfg.Ingest.AES67.MulticastGroup = ""
  208. cfg.Ingest.AES67.Port = 0
  209. cfg.Ingest.AES67.Discovery.StreamName = "AES67-MAIN"
  210. cfg.Ingest.AES67.Discovery.SAPPort = 70000
  211. if err := cfg.Validate(); err == nil {
  212. t.Fatal("expected discovery sapPort validation error")
  213. }
  214. }
  215. func TestValidateRejectsUnsupportedIngestPCMShape(t *testing.T) {
  216. cfg := Default()
  217. cfg.Ingest.Stdin.SampleRateHz = 0
  218. if err := cfg.Validate(); err == nil {
  219. t.Fatal("expected sampleRate error")
  220. }
  221. cfg = Default()
  222. cfg.Ingest.HTTPRaw.Channels = 6
  223. if err := cfg.Validate(); err == nil {
  224. t.Fatal("expected channels error")
  225. }
  226. cfg = Default()
  227. cfg.Ingest.Stdin.Format = "f32le"
  228. if err := cfg.Validate(); err == nil {
  229. t.Fatal("expected format error")
  230. }
  231. }
  232. func TestValidateRejectsUnsupportedIcecastDecoder(t *testing.T) {
  233. cfg := Default()
  234. cfg.Ingest.Icecast.Decoder = "mystery"
  235. if err := cfg.Validate(); err == nil {
  236. t.Fatal("expected decoder error")
  237. }
  238. }
  239. func TestValidateAcceptsIcecastDecoderFallbackAlias(t *testing.T) {
  240. cfg := Default()
  241. cfg.Ingest.Icecast.Decoder = "fallback"
  242. if err := cfg.Validate(); err != nil {
  243. t.Fatalf("expected fallback alias to be accepted: %v", err)
  244. }
  245. }
  246. func TestValidateRejectsIcecastRadioTextMaxLenOutOfRange(t *testing.T) {
  247. cfg := Default()
  248. cfg.Ingest.Icecast.RadioText.MaxLen = 65
  249. if err := cfg.Validate(); err == nil {
  250. t.Fatal("expected maxLen error")
  251. }
  252. }
  253. func TestValidateRejectsReconnectWithMissingBackoff(t *testing.T) {
  254. cfg := Default()
  255. cfg.Ingest.Reconnect.Enabled = true
  256. cfg.Ingest.Reconnect.InitialBackoffMs = 0
  257. if err := cfg.Validate(); err == nil {
  258. t.Fatal("expected reconnect backoff error")
  259. }
  260. }
  261. func TestValidateRejectsZeroMpxGain(t *testing.T) {
  262. cfg := Default()
  263. cfg.FM.MpxGain = 0
  264. if err := cfg.Validate(); err == nil {
  265. t.Fatal("expected mpxGain error")
  266. }
  267. }
  268. func TestValidateRejectsInvalidStereoMode(t *testing.T) {
  269. cfg := Default()
  270. cfg.FM.StereoMode = "weird"
  271. if err := cfg.Validate(); err == nil {
  272. t.Fatal("expected stereoMode validation error")
  273. }
  274. }
  275. func TestValidateAcceptsStereoModes(t *testing.T) {
  276. for _, mode := range []string{"DSB", "SSB", "VSB"} {
  277. cfg := Default()
  278. cfg.FM.StereoMode = mode
  279. if err := cfg.Validate(); err != nil {
  280. t.Fatalf("mode %s should validate: %v", mode, err)
  281. }
  282. }
  283. }