Wideband autonomous SDR analysis engine forked from sdr-visual-suite
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.

139 lines
5.1KB

  1. package main
  2. import (
  3. "testing"
  4. "sdr-wideband-suite/internal/pipeline"
  5. )
  6. func TestBuildMonitorWindowSummaryCountsCandidates(t *testing.T) {
  7. windows := []pipeline.MonitorWindow{
  8. {Index: 0, Label: "primary", StartHz: 100, EndHz: 200, CenterHz: 150, SpanHz: 100},
  9. {Index: 1, Label: "secondary", StartHz: 300, EndHz: 400, CenterHz: 350, SpanHz: 100},
  10. }
  11. candidates := []pipeline.Candidate{
  12. {ID: 1, CenterHz: 150, BandwidthHz: 20},
  13. {ID: 2, CenterHz: 320, BandwidthHz: 10},
  14. }
  15. summary := buildMonitorWindowSummary(windows, nil, candidates)
  16. if len(summary) != 2 {
  17. t.Fatalf("expected 2 window summaries, got %d", len(summary))
  18. }
  19. if summary[0].Candidates != 1 || summary[1].Candidates != 1 {
  20. t.Fatalf("unexpected candidate counts: %+v", summary)
  21. }
  22. }
  23. func TestBuildMonitorWindowSummaryPreservesStatsCounts(t *testing.T) {
  24. stats := []pipeline.MonitorWindowStats{
  25. {Index: 0, Label: "primary", StartHz: 100, EndHz: 200, CenterHz: 150, SpanHz: 100, Candidates: 2, Planned: 1},
  26. }
  27. windows := []pipeline.MonitorWindow{
  28. {Index: 0, Label: "primary", StartHz: 100, EndHz: 200, CenterHz: 150, SpanHz: 100},
  29. }
  30. candidates := []pipeline.Candidate{
  31. {ID: 1, CenterHz: 150, BandwidthHz: 20},
  32. }
  33. summary := buildMonitorWindowSummary(windows, stats, candidates)
  34. if len(summary) != 1 {
  35. t.Fatalf("expected 1 window summary, got %d", len(summary))
  36. }
  37. if summary[0].Candidates != 2 {
  38. t.Fatalf("expected candidates to stay at 2, got %d", summary[0].Candidates)
  39. }
  40. if summary[0].Planned != 1 {
  41. t.Fatalf("expected planned to stay at 1, got %d", summary[0].Planned)
  42. }
  43. }
  44. func TestBuildWindowOutcomeSummaryTracksPressureByWindowAndZone(t *testing.T) {
  45. windows := []pipeline.MonitorWindow{
  46. {Index: 0, Label: "alpha", Zone: "north", StartHz: 100, EndHz: 200},
  47. {Index: 1, Label: "beta", Zone: "south", StartHz: 300, EndHz: 400},
  48. }
  49. match0 := pipeline.MonitorWindowMatch{Index: 0, Label: "alpha", Zone: "north"}
  50. match1 := pipeline.MonitorWindowMatch{Index: 1, Label: "beta", Zone: "south"}
  51. workItems := []pipeline.RefinementWorkItem{
  52. {
  53. Candidate: pipeline.Candidate{ID: 1, MonitorMatches: []pipeline.MonitorWindowMatch{match0}},
  54. Admission: &pipeline.PriorityAdmission{Class: pipeline.AdmissionClassAdmit},
  55. },
  56. {
  57. Candidate: pipeline.Candidate{ID: 2, MonitorMatches: []pipeline.MonitorWindowMatch{match0}},
  58. Admission: &pipeline.PriorityAdmission{Class: pipeline.AdmissionClassHold},
  59. },
  60. {
  61. Candidate: pipeline.Candidate{ID: 3, MonitorMatches: []pipeline.MonitorWindowMatch{match1}},
  62. Admission: &pipeline.PriorityAdmission{Class: pipeline.AdmissionClassDisplace},
  63. },
  64. {
  65. Candidate: pipeline.Candidate{ID: 4, MonitorMatches: []pipeline.MonitorWindowMatch{match1}},
  66. Admission: &pipeline.PriorityAdmission{Class: pipeline.AdmissionClassDefer},
  67. },
  68. }
  69. decisions := []pipeline.SignalDecision{
  70. {
  71. Candidate: pipeline.Candidate{ID: 1},
  72. ShouldRecord: true,
  73. RecordWindow: &match0,
  74. RecordAdmission: &pipeline.PriorityAdmission{Class: pipeline.AdmissionClassAdmit},
  75. },
  76. {
  77. Candidate: pipeline.Candidate{ID: 2},
  78. ShouldRecord: false,
  79. RecordWindow: &match0,
  80. RecordAdmission: &pipeline.PriorityAdmission{Class: pipeline.AdmissionClassDisplace},
  81. },
  82. {
  83. Candidate: pipeline.Candidate{ID: 3},
  84. ShouldAutoDecode: true,
  85. DecodeWindow: &match1,
  86. DecodeAdmission: &pipeline.PriorityAdmission{Class: pipeline.AdmissionClassHold},
  87. },
  88. {
  89. Candidate: pipeline.Candidate{ID: 4},
  90. ShouldAutoDecode: false,
  91. DecodeWindow: &match1,
  92. DecodeAdmission: &pipeline.PriorityAdmission{Class: pipeline.AdmissionClassDefer},
  93. },
  94. }
  95. summary := buildWindowSummary(pipeline.RefinementPlan{MonitorWindows: windows}, nil, nil, workItems, decisions)
  96. if summary == nil || summary.Outcomes == nil {
  97. t.Fatalf("expected outcome summary to be populated")
  98. }
  99. if len(summary.Outcomes.Windows) != 2 {
  100. t.Fatalf("expected 2 window outcomes, got %d", len(summary.Outcomes.Windows))
  101. }
  102. win0 := summary.Outcomes.Windows[0]
  103. win1 := summary.Outcomes.Windows[1]
  104. if win0.Refinement.Admit != 1 || win0.Refinement.Hold != 1 {
  105. t.Fatalf("unexpected refinement outcomes for window 0: %+v", win0.Refinement)
  106. }
  107. if win0.Record.Admit != 1 || win0.Record.Displace != 1 || win0.Record.Enabled != 1 {
  108. t.Fatalf("unexpected record outcomes for window 0: %+v", win0.Record)
  109. }
  110. if win1.Refinement.Displace != 1 || win1.Refinement.Defer != 1 {
  111. t.Fatalf("unexpected refinement outcomes for window 1: %+v", win1.Refinement)
  112. }
  113. if win1.Decode.Hold != 1 || win1.Decode.Defer != 1 || win1.Decode.Enabled != 1 {
  114. t.Fatalf("unexpected decode outcomes for window 1: %+v", win1.Decode)
  115. }
  116. if len(summary.Outcomes.Zones) != 2 {
  117. t.Fatalf("expected 2 zone outcomes, got %d", len(summary.Outcomes.Zones))
  118. }
  119. for _, zone := range summary.Outcomes.Zones {
  120. switch zone.Zone {
  121. case "north":
  122. if zone.Refinement.Admit != 1 || zone.Refinement.Hold != 1 {
  123. t.Fatalf("unexpected north zone refinement: %+v", zone.Refinement)
  124. }
  125. case "south":
  126. if zone.Refinement.Displace != 1 || zone.Refinement.Defer != 1 {
  127. t.Fatalf("unexpected south zone refinement: %+v", zone.Refinement)
  128. }
  129. default:
  130. t.Fatalf("unexpected zone %q", zone.Zone)
  131. }
  132. }
  133. }