Skip to content

Commit 074d724

Browse files
author
Datanoise
committed
relay: add tests covering freshness gate and Ogg page validation
Covers the regressions just fixed: - Subscribe on an idle stream returns Head, not a stale burst. - Subscribe on an active stream still bursts as requested. - FreshHead clamps the burst window after BeginSession. - AlignToOggPage rejects 'OggS' with a non-zero version byte and finds the real page that follows. - Broadcast does not poison PageOffsets with false positives but does record a real page when the version byte is 0. - BeginSession wipes OggHead, LastPageOffset, PageOffsets, PageIndex and advances FreshHead.
1 parent 8a6b8df commit 074d724

1 file changed

Lines changed: 192 additions & 0 deletions

File tree

relay/stream_test.go

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
package relay
2+
3+
import (
4+
"sync"
5+
"testing"
6+
"time"
7+
)
8+
9+
// newTestStream builds a Stream of the same shape that GetOrCreateStream
10+
// produces, but without needing the global Relay wired up.
11+
func newTestStream(ogg bool) *Stream {
12+
return &Stream{
13+
MountName: "/test",
14+
listeners: make(map[string]chan struct{}),
15+
Buffer: NewCircularBuffer(64 * 1024),
16+
Started: time.Now(),
17+
ContentType: "audio/ogg",
18+
IsOggStream: ogg,
19+
Enabled: true,
20+
PageOffsets: make([]int64, 32),
21+
mu: sync.RWMutex{},
22+
}
23+
}
24+
25+
// fakeRelay lets Broadcast update its atomic counters without dragging the
26+
// HistoryManager / database into a unit test.
27+
func fakeRelay() *Relay { return &Relay{} }
28+
29+
// TestSubscribeSkipsBurstWhenSourceIdle verifies bug A's fix: if the source
30+
// hasn't written audio for >2s, a new subscriber must start at Head and not
31+
// be bursted with stale buffer contents.
32+
func TestSubscribeSkipsBurstWhenSourceIdle(t *testing.T) {
33+
s := newTestStream(false)
34+
r := fakeRelay()
35+
36+
// Pretend a source was active in the past and left audio in the buffer.
37+
s.Broadcast(make([]byte, 8192), r)
38+
s.mu.Lock()
39+
s.LastDataReceived = time.Now().Add(-10 * time.Second)
40+
s.mu.Unlock()
41+
42+
head := s.Buffer.SnapshotHead()
43+
start, _ := s.Subscribe("listener-1", 4096)
44+
if start != head {
45+
t.Fatalf("idle-source subscribe should start at Head=%d, got %d (would replay %d stale bytes)",
46+
head, start, head-start)
47+
}
48+
}
49+
50+
// TestSubscribeBurstsWhenSourceFresh verifies the burst still works on an
51+
// active stream — the freshness gate must not break the normal "instant
52+
// playback" path.
53+
func TestSubscribeBurstsWhenSourceFresh(t *testing.T) {
54+
s := newTestStream(false)
55+
r := fakeRelay()
56+
57+
// LastDataReceived is set by Broadcast itself to time.Now().
58+
s.Broadcast(make([]byte, 8192), r)
59+
60+
head := s.Buffer.SnapshotHead()
61+
start, _ := s.Subscribe("listener-1", 4096)
62+
if start >= head {
63+
t.Fatalf("fresh-source subscribe should burst, start=%d head=%d", start, head)
64+
}
65+
if head-start != 4096 {
66+
t.Fatalf("expected 4096-byte burst, got %d", head-start)
67+
}
68+
}
69+
70+
// TestSubscribeNeverWalksBackPastFreshHead verifies bug E's fix: even if the
71+
// Ogg page-alignment fallback would otherwise pick an offset older than the
72+
// freshness boundary, Subscribe must clamp it.
73+
func TestSubscribeNeverWalksBackPastFreshHead(t *testing.T) {
74+
s := newTestStream(true)
75+
r := fakeRelay()
76+
77+
// Stale audio from a previous session.
78+
s.Broadcast(make([]byte, 16*1024), r)
79+
80+
// New session starts — FreshHead advances to the current Head, all old
81+
// page offsets must be invalidated.
82+
s.BeginSession()
83+
freshHead := s.Buffer.SnapshotHead()
84+
85+
// Subscribe before any new audio has arrived. LastDataReceived is still
86+
// from the old session, so the >2s freshness gate alone won't trip; we
87+
// rely on FreshHead.
88+
s.mu.Lock()
89+
s.LastDataReceived = time.Now() // pretend source is "live"
90+
s.mu.Unlock()
91+
92+
start, _ := s.Subscribe("listener-1", 8192)
93+
if start < freshHead {
94+
t.Fatalf("subscribe returned start=%d which is BEFORE FreshHead=%d (would replay stale audio)",
95+
start, freshHead)
96+
}
97+
}
98+
99+
// TestAlignToOggPageRejectsFalseMagic verifies bug B's fix: bare "OggS"
100+
// bytes inside a payload are not treated as page boundaries — only the
101+
// magic followed by a 0 version byte counts.
102+
func TestAlignToOggPageRejectsFalseMagic(t *testing.T) {
103+
buf := NewCircularBuffer(4096)
104+
105+
// False positive: "OggS" followed by version byte 0xFF (could be Opus
106+
// payload data). Must NOT be picked as a page boundary.
107+
false1 := []byte{0x00, 0x11, 'O', 'g', 'g', 'S', 0xFF, 0x22, 0x33, 0x44, 0x55, 0x66}
108+
buf.Write(false1)
109+
110+
// Real page: "OggS" followed by version byte 0x00.
111+
realHeaderOffset := buf.SnapshotHead() + 4
112+
real := []byte{0xAA, 0xBB, 0xCC, 0xDD, 'O', 'g', 'g', 'S', 0x00, 0x00, 0x00, 0x00}
113+
buf.Write(real)
114+
115+
got := buf.AlignToOggPage(0)
116+
if got != realHeaderOffset {
117+
t.Fatalf("AlignToOggPage should have skipped the version!=0 false positive and "+
118+
"returned the real page at offset %d, got %d", realHeaderOffset, got)
119+
}
120+
}
121+
122+
// TestBroadcastIgnoresFalseOggMagic verifies the same validation inside the
123+
// Broadcast page-tracking path: a false-positive "OggS" in payload bytes
124+
// must not poison PageOffsets.
125+
func TestBroadcastIgnoresFalseOggMagic(t *testing.T) {
126+
s := newTestStream(true)
127+
r := fakeRelay()
128+
129+
// All-zeros payload with a "OggS" sequence followed by 0xFF (not a real
130+
// Ogg version). No tracked page should be recorded.
131+
payload := make([]byte, 64)
132+
payload[10] = 'O'
133+
payload[11] = 'g'
134+
payload[12] = 'g'
135+
payload[13] = 'S'
136+
payload[14] = 0xFF
137+
s.Broadcast(payload, r)
138+
139+
for i, po := range s.PageOffsets {
140+
if po != 0 {
141+
t.Fatalf("PageOffsets[%d]=%d — a false-positive OggS was tracked", i, po)
142+
}
143+
}
144+
145+
// Now a real page (version byte == 0). Must be tracked.
146+
real := make([]byte, 32)
147+
real[5] = 'O'
148+
real[6] = 'g'
149+
real[7] = 'g'
150+
real[8] = 'S'
151+
real[9] = 0x00
152+
expected := s.Buffer.SnapshotHead() + 5
153+
s.Broadcast(real, r)
154+
155+
if s.LastPageOffset != expected {
156+
t.Fatalf("LastPageOffset=%d, expected %d", s.LastPageOffset, expected)
157+
}
158+
}
159+
160+
// TestBeginSessionWipesOggState verifies bug C's fix: after a producer
161+
// restart, stale Ogg headers/page tracking must not survive.
162+
func TestBeginSessionWipesOggState(t *testing.T) {
163+
s := newTestStream(true)
164+
165+
s.SetOggHead([]byte{1, 2, 3, 4}, 100)
166+
s.mu.Lock()
167+
s.LastPageOffset = 999
168+
s.PageOffsets[0] = 999
169+
s.PageIndex = 7
170+
s.mu.Unlock()
171+
172+
s.BeginSession()
173+
174+
if got := s.GetOggHead(); got != nil {
175+
t.Fatalf("OggHead should be nil after BeginSession, got %v", got)
176+
}
177+
s.mu.RLock()
178+
defer s.mu.RUnlock()
179+
if s.LastPageOffset != 0 {
180+
t.Fatalf("LastPageOffset should be reset, got %d", s.LastPageOffset)
181+
}
182+
if s.PageOffsets[0] != 0 {
183+
t.Fatalf("PageOffsets[0] should be reset, got %d", s.PageOffsets[0])
184+
}
185+
if s.PageIndex != 0 {
186+
t.Fatalf("PageIndex should be reset, got %d", s.PageIndex)
187+
}
188+
head := s.Buffer.SnapshotHead()
189+
if s.FreshHead != head {
190+
t.Fatalf("FreshHead=%d should equal current Head=%d after BeginSession", s.FreshHead, head)
191+
}
192+
}

0 commit comments

Comments
 (0)