|
- package rds
-
- import (
- "math"
- "strings"
- "testing"
- "time"
- )
-
- func TestCRC10KnownVector(t *testing.T) {
- c := crc10(0x1234)
- if c > 0x3FF { t.Fatalf("CRC exceeds 10 bits: %x", c) }
- }
-
- func TestEncodeBlockProduces26Bits(t *testing.T) {
- block := encodeBlock(0x1234, 'A')
- if block>>26 != 0 { t.Fatalf("block exceeds 26 bits: %x", block) }
- if uint16(block>>10) != 0x1234 { t.Fatalf("data mismatch") }
- }
-
- func TestBuildGroup0A(t *testing.T) {
- cfg := &RDSConfig{PI: 0x1234, PS: "TESTFM"}
- g := buildGroup0A(cfg, 0, [2]uint8{})
- if g[0] != 0x1234 { t.Fatalf("block A not PI: %x", g[0]) }
- if byte(g[3]>>8) != 'T' || byte(g[3]&0xFF) != 'E' { t.Fatal("wrong PS chars") }
- }
-
- func TestBuildGroup2A(t *testing.T) {
- g := buildGroup2A(0x1234, 0, false, false, 0, "Hello World")
- if g[0] != 0x1234 { t.Fatal("block A not PI") }
- if (g[1]>>12)&0x0F != 2 { t.Fatal("wrong group type") }
- }
-
- func TestBuildGroupUsesConfiguredPI(t *testing.T) {
- cfg0A := &RDSConfig{PI: 0xBEEF, PS: "TEST"}
- if buildGroup0A(cfg0A, 0, [2]uint8{})[0] != 0xBEEF { t.Fatal("PI mismatch 0A") }
- if buildGroup2A(0xCAFE, 0, false, false, 0, "Hello")[0] != 0xCAFE { t.Fatal("PI mismatch 2A") }
- }
-
- func TestEncoderGenerate(t *testing.T) {
- cfg := DefaultConfig(); cfg.SampleRate = 228000
- enc, err := NewEncoder(cfg)
- if err != nil { t.Fatal(err) }
- samples := enc.Generate(1024)
- if len(samples) != 1024 { t.Fatal("wrong length") }
- var energy, maxAbs float64
- for _, s := range samples {
- energy += s * s
- if math.Abs(s) > maxAbs { maxAbs = math.Abs(s) }
- }
- if energy == 0 { t.Fatal("zero energy") }
- // Unity output: peak should be close to 1.0
- if maxAbs > 3.0 { t.Fatalf("exceeds unity: %.6f", maxAbs) }
- }
-
- func TestEncoderNextSample(t *testing.T) {
- cfg := DefaultConfig(); cfg.SampleRate = 228000
- enc, _ := NewEncoder(cfg)
- s := enc.NextSample()
- // Should not panic and should produce a value
- if math.IsNaN(s) { t.Fatal("NaN") }
- }
-
- func TestEncoderReset(t *testing.T) {
- cfg := DefaultConfig(); cfg.SampleRate = 228000
- enc, _ := NewEncoder(cfg)
- a := enc.NextSample()
- for i := 0; i < 100; i++ { enc.NextSample() }
- enc.Reset()
- b := enc.NextSample()
- if math.Abs(a-b) > 1e-9 { t.Fatalf("reset failed: %v vs %v", a, b) }
- }
-
- func TestGroupSchedulerCycles(t *testing.T) {
- cfg := DefaultConfig(); cfg.PS = "TESTPS"; cfg.RT = "short"
- gs := newGroupScheduler(cfg)
- for i := 0; i < 40; i++ { _ = gs.NextGroup() }
- }
-
- func TestNormalizePS(t *testing.T) {
- if normalizePS("radiox") != "RADIOX " { t.Fatal("wrong PS") }
- }
-
- func TestNormalizeRT(t *testing.T) {
- if len(normalizeRT(strings.Repeat("a", 80))) != 64 { t.Fatal("wrong RT length") }
- }
-
-
- func TestRTSegmentCount(t *testing.T) {
- if rtSegmentCount("Hi") != 1 { t.Fatal("expected 1") }
- if rtSegmentCount("Hello World!") != 3 { t.Fatal("expected 3") }
- if rtSegmentCount(strings.Repeat("x", 64)) != 16 { t.Fatal("expected 16") }
- }
-
- // --- New group tests ---
-
- func TestParseRTPlus(t *testing.T) {
- t1, t2, has2 := ParseRTPlus("Depeche Mode - Enjoy The Silence", " - ")
- if !has2 { t.Fatal("expected 2 tags") }
- if t1.ContentType != RTPlusItemArtist { t.Fatal("tag1 should be artist") }
- if t2.ContentType != RTPlusItemTitle { t.Fatal("tag2 should be title") }
- if string(normalizeRT("Depeche Mode - Enjoy The Silence")[t1.Start:t1.Start+t1.Length]) != "Depeche Mode" {
- t.Fatalf("artist mismatch: start=%d len=%d", t1.Start, t1.Length)
- }
- if string(normalizeRT("Depeche Mode - Enjoy The Silence")[t2.Start:t2.Start+t2.Length]) != "Enjoy The Silence" {
- t.Fatalf("title mismatch: start=%d len=%d", t2.Start, t2.Length)
- }
- }
-
- func TestParseRTPlusNoSeparator(t *testing.T) {
- t1, _, has2 := ParseRTPlus("Just a station message", " - ")
- if has2 { t.Fatal("expected 1 tag") }
- if t1.ContentType != RTPlusItemTitle { t.Fatal("should be title") }
- if t1.Length != 22 { t.Fatalf("wrong length: %d", t1.Length) }
- }
-
- func TestAFEncoding(t *testing.T) {
- c := freqToAF(87.6)
- if c != 1 { t.Fatalf("87.6 MHz should be AF code 1, got %d", c) }
- c = freqToAF(107.9)
- if c != 204 { t.Fatalf("107.9 MHz should be AF code 204, got %d", c) }
- c = freqToAF(100.0)
- if c != 125 { t.Fatalf("100.0 MHz should be AF code 125, got %d", c) }
- }
-
- func TestAFListPairs(t *testing.T) {
- pairs := buildAFList([]float64{93.3, 95.7, 99.1})
- if len(pairs) != 2 { t.Fatalf("expected 2 AF pairs, got %d", len(pairs)) }
- // First pair: count indicator + first AF
- if pairs[0][0] != 224+3 { t.Fatalf("expected count indicator 227, got %d", pairs[0][0]) }
- }
-
- func TestBuildGroup4A(t *testing.T) {
- // Known date: 2026-04-11 14:30 UTC, offset +2 (CEST)
- tm := time.Date(2026, 4, 11, 14, 30, 0, 0, time.UTC)
- g := buildGroup4A(0x1234, 0, false, tm, 4)
- if g[0] != 0x1234 { t.Fatal("PI mismatch") }
- if (g[1]>>12)&0xF != 4 { t.Fatal("wrong group type") }
- // Just verify it doesn't panic and produces non-zero blocks
- if g[2] == 0 && g[3] == 0 { t.Fatal("CT blocks are zero") }
- }
-
- func TestBuildGroup10A(t *testing.T) {
- g := buildGroup10A(0x1234, 10, false, false, 0, "INDIE")
- if g[0] != 0x1234 { t.Fatal("PI mismatch") }
- if (g[1]>>12)&0xF != 0xA { t.Fatal("wrong group type") }
- if byte(g[2]>>8) != 'I' || byte(g[2]&0xFF) != 'N' { t.Fatal("wrong PTYN chars seg 0") }
- }
-
- func TestFullSchedulerAllGroups(t *testing.T) {
- cfg := DefaultConfig()
- cfg.PS = "TESTPS"
- cfg.RT = "Artist - Title"
- cfg.PTYN = "ROCK"
- cfg.CTEnabled = true
- cfg.RTPlusEnabled = true
- cfg.RTPlusSeparator = " - "
- cfg.AF = []float64{93.3, 95.7}
- cfg.EON = []EONEntry{{PI: 0x5678, PS: "OTHER", AF: []float64{88.0}}}
-
- gs := newGroupScheduler(cfg)
-
- // Run 200 groups — should cover all types without panic
- seen := map[int]bool{}
- for i := 0; i < 200; i++ {
- g := gs.NextGroup()
- groupType := int((g[1] >> 12) & 0xF)
- seen[groupType] = true
- }
-
- // Verify all expected group types were seen
- for _, gt := range []int{0, 2, 3, 4, 0xA, 0xB, 0xE} {
- if !seen[gt] {
- t.Errorf("group type %d never scheduled in 200 groups", gt)
- }
- }
- t.Logf("Seen group types: %v", seen)
- }
|