Go-based FM stereo transmitter with RDS, Windows-first and cross-platform
25'ten fazla konu seçemezsiniz Konular bir harf veya rakamla başlamalı, kısa çizgiler ('-') içerebilir ve en fazla 35 karakter uzunluğunda olabilir.

239 satır
7.9KB

  1. package app
  2. import (
  3. "testing"
  4. cfgpkg "github.com/jan/fm-rds-tx/internal/config"
  5. "github.com/jan/fm-rds-tx/internal/output"
  6. "github.com/jan/fm-rds-tx/internal/platform"
  7. )
  8. func TestEngineRuntimeStateReporting(t *testing.T) {
  9. e := NewEngine(cfgpkg.Default(), platform.NewSimulatedDriver(nil))
  10. if got := e.Stats().State; got != string(RuntimeStateIdle) {
  11. t.Fatalf("expected initial state idle, got %s", got)
  12. }
  13. e.setRuntimeState(RuntimeStatePrebuffering)
  14. if got := e.Stats().State; got != string(RuntimeStatePrebuffering) {
  15. t.Fatalf("expected prebuffering, got %s", got)
  16. }
  17. e.setRuntimeState(RuntimeStateRunning)
  18. if got := e.currentRuntimeState(); got != RuntimeStateRunning {
  19. t.Fatalf("currentRuntimeState mismatch: %s", got)
  20. }
  21. }
  22. func TestEngineRuntimeStateTransitions(t *testing.T) {
  23. e := NewEngine(cfgpkg.Default(), platform.NewSimulatedDriver(nil))
  24. e.setRuntimeState(RuntimeStatePrebuffering)
  25. queue := output.QueueStats{Depth: 1, FillLevel: 0.75, Health: output.QueueHealthNormal}
  26. e.evaluateRuntimeState(queue, false)
  27. if got := e.currentRuntimeState(); got != RuntimeStateRunning {
  28. t.Fatalf("expected running after full buffer, got %s", got)
  29. }
  30. queue.Health = output.QueueHealthCritical
  31. for i := 0; i < queueCriticalStreakThreshold; i++ {
  32. e.evaluateRuntimeState(queue, false)
  33. }
  34. if got := e.currentRuntimeState(); got != RuntimeStateDegraded {
  35. t.Fatalf("expected degraded on queue critical streak, got %s", got)
  36. }
  37. queue.Health = output.QueueHealthNormal
  38. e.evaluateRuntimeState(queue, false)
  39. if got := e.currentRuntimeState(); got != RuntimeStateRunning {
  40. t.Fatalf("expected running once queue healthy, got %s", got)
  41. }
  42. e.evaluateRuntimeState(queue, true)
  43. if got := e.currentRuntimeState(); got != RuntimeStateDegraded {
  44. t.Fatalf("expected degraded when late buffers seen, got %s", got)
  45. }
  46. }
  47. func TestEngineRuntimeStateMuteOnPersistentQueueCritical(t *testing.T) {
  48. e := NewEngine(cfgpkg.Default(), platform.NewSimulatedDriver(nil))
  49. e.setRuntimeState(RuntimeStateRunning)
  50. queue := output.QueueStats{Depth: 1, Health: output.QueueHealthCritical}
  51. for i := 0; i < queueMutedStreakThreshold; i++ {
  52. e.evaluateRuntimeState(queue, false)
  53. }
  54. if got := e.currentRuntimeState(); got != RuntimeStateMuted {
  55. t.Fatalf("expected muted after prolonged queue critical, got %s", got)
  56. }
  57. muteFault := e.LastFault()
  58. if muteFault == nil {
  59. t.Fatal("expected fault recorded for the mute transition")
  60. }
  61. if muteFault.Reason != FaultReasonQueueCritical {
  62. t.Fatalf("expected queue critical reason, got %s", muteFault.Reason)
  63. }
  64. if muteFault.Severity != FaultSeverityMuted {
  65. t.Fatalf("expected muted severity, got %s", muteFault.Severity)
  66. }
  67. queue.Health = output.QueueHealthNormal
  68. for i := 0; i < queueMutedRecoveryThreshold-1; i++ {
  69. e.evaluateRuntimeState(queue, false)
  70. if got := e.currentRuntimeState(); got != RuntimeStateMuted {
  71. t.Fatalf("expected still muted while recovery window builds, got %s", got)
  72. }
  73. }
  74. e.evaluateRuntimeState(queue, false)
  75. if got := e.currentRuntimeState(); got != RuntimeStateDegraded {
  76. t.Fatalf("expected degrade once mute recovery threshold reached, got %s", got)
  77. }
  78. recoveryFault := e.LastFault()
  79. if recoveryFault == nil {
  80. t.Fatal("expected recovery fault entry after leaving mute")
  81. }
  82. if recoveryFault.Severity != FaultSeverityDegraded {
  83. t.Fatalf("expected degraded severity for recovery event, got %s", recoveryFault.Severity)
  84. }
  85. if recoveryFault.Reason != FaultReasonQueueCritical {
  86. t.Fatalf("expected queue critical reason for recovery event, got %s", recoveryFault.Reason)
  87. }
  88. e.evaluateRuntimeState(queue, false)
  89. if got := e.currentRuntimeState(); got != RuntimeStateRunning {
  90. t.Fatalf("expected running after recovery, got %s", got)
  91. }
  92. }
  93. func TestEngineFaultsAfterMutedCriticalStreak(t *testing.T) {
  94. e := NewEngine(cfgpkg.Default(), platform.NewSimulatedDriver(nil))
  95. e.setRuntimeState(RuntimeStateRunning)
  96. queue := output.QueueStats{Depth: 1, Health: output.QueueHealthCritical}
  97. for i := 0; i < queueMutedStreakThreshold; i++ {
  98. e.evaluateRuntimeState(queue, false)
  99. }
  100. if got := e.currentRuntimeState(); got != RuntimeStateMuted {
  101. t.Fatalf("expected muted after draining critical streak, got %s", got)
  102. }
  103. triggered := false
  104. for i := 0; i < queueFaultedStreakThreshold; i++ {
  105. e.evaluateRuntimeState(queue, false)
  106. if e.currentRuntimeState() == RuntimeStateFaulted {
  107. triggered = true
  108. break
  109. }
  110. }
  111. if !triggered {
  112. t.Fatalf("expected faulted after %d extra critical checks", queueFaultedStreakThreshold)
  113. }
  114. if got := e.currentRuntimeState(); got != RuntimeStateFaulted {
  115. t.Fatalf("expected faulted state, got %s", got)
  116. }
  117. fault := e.LastFault()
  118. if fault == nil {
  119. t.Fatal("expected recorded fault")
  120. }
  121. if fault.Severity != FaultSeverityFaulted {
  122. t.Fatalf("expected faulted severity, got %s", fault.Severity)
  123. }
  124. if fault.Reason != FaultReasonQueueCritical {
  125. t.Fatalf("expected queue critical reason, got %s", fault.Reason)
  126. }
  127. }
  128. func TestRuntimeTransitionCounters(t *testing.T) {
  129. e := NewEngine(cfgpkg.Default(), platform.NewSimulatedDriver(nil))
  130. if got := e.Stats().DegradedTransitions; got != 0 {
  131. t.Fatalf("expected zero transitions initially, got %d", got)
  132. }
  133. if got := e.Stats().FaultCount; got != 0 {
  134. t.Fatalf("expected zero faults initially, got %d", got)
  135. }
  136. e.setRuntimeState(RuntimeStateDegraded)
  137. if got := e.Stats().DegradedTransitions; got != 1 {
  138. t.Fatalf("expected one degraded transition, got %d", got)
  139. }
  140. e.setRuntimeState(RuntimeStateMuted)
  141. if got := e.Stats().MutedTransitions; got != 1 {
  142. t.Fatalf("expected one mute transition, got %d", got)
  143. }
  144. e.setRuntimeState(RuntimeStateFaulted)
  145. if got := e.Stats().FaultedTransitions; got != 1 {
  146. t.Fatalf("expected one faulted transition, got %d", got)
  147. }
  148. e.recordFault(FaultReasonQueueCritical, FaultSeverityWarn, "audit")
  149. if got := e.Stats().FaultCount; got != 1 {
  150. t.Fatalf("expected one recorded fault, got %d", got)
  151. }
  152. }
  153. func TestEngineTransitionHistory(t *testing.T) {
  154. e := NewEngine(cfgpkg.Default(), platform.NewSimulatedDriver(nil))
  155. e.setRuntimeState(RuntimeStateRunning)
  156. e.setRuntimeState(RuntimeStateDegraded)
  157. e.setRuntimeState(RuntimeStateMuted)
  158. history := e.Stats().TransitionHistory
  159. if len(history) != 3 {
  160. t.Fatalf("expected 3 transitions recorded, got %d", len(history))
  161. }
  162. if history[0].From != RuntimeStateIdle || history[0].To != RuntimeStateRunning {
  163. t.Fatalf("unexpected first transition: %+v", history[0])
  164. }
  165. if history[0].Severity != "ok" {
  166. t.Fatalf("expected ok severity for running transition, got %s", history[0].Severity)
  167. }
  168. if history[1].To != RuntimeStateDegraded || history[1].Severity != "warn" {
  169. t.Fatalf("expected degraded transition with warn severity, got %+v", history[1])
  170. }
  171. if history[2].To != RuntimeStateMuted || history[2].Severity != "warn" {
  172. t.Fatalf("expected muted transition with warn severity, got %+v", history[2])
  173. }
  174. }
  175. func TestEngineResetFaultRequiresFaultedState(t *testing.T) {
  176. e := NewEngine(cfgpkg.Default(), platform.NewSimulatedDriver(nil))
  177. if err := e.ResetFault(); err == nil {
  178. t.Fatal("expected error when resetting non-faulted state")
  179. }
  180. }
  181. func TestEngineResetFaultTransitionsToDegraded(t *testing.T) {
  182. e := NewEngine(cfgpkg.Default(), platform.NewSimulatedDriver(nil))
  183. e.criticalStreak.Store(7)
  184. e.mutedRecoveryStreak.Store(3)
  185. e.mutedFaultStreak.Store(1)
  186. e.setRuntimeState(RuntimeStateFaulted)
  187. if err := e.ResetFault(); err != nil {
  188. t.Fatalf("reset fault failed: %v", err)
  189. }
  190. if got := e.currentRuntimeState(); got != RuntimeStateDegraded {
  191. t.Fatalf("expected degraded after reset, got %s", got)
  192. }
  193. if e.criticalStreak.Load() != 0 {
  194. t.Fatalf("expected critical streak reset, got %d", e.criticalStreak.Load())
  195. }
  196. if e.mutedRecoveryStreak.Load() != 0 {
  197. t.Fatalf("expected mute recovery streak reset, got %d", e.mutedRecoveryStreak.Load())
  198. }
  199. if e.mutedFaultStreak.Load() != 0 {
  200. t.Fatalf("expected mute fault streak reset, got %d", e.mutedFaultStreak.Load())
  201. }
  202. if err := e.ResetFault(); err == nil {
  203. t.Fatal("expected error when resetting after recovery")
  204. }
  205. }