telegabber/calls/audio/tg_to_xmpp_test.go
2026-05-27 08:37:47 -07:00

125 lines
3.1 KiB
Go

package audio
import (
"sync"
"testing"
)
// shared between tg->xmpp and xmpp->tg tests
type fakeNtg struct {
mu sync.Mutex
handler NtgFrameHandler
unregistered int // count of cancel() invocations returned by OnFrame
sentPCM [][]byte
micCalls int
clears int
micCallCapture []bool // ordered record of capture-flag arg per SetExternalMicrophone call
sendErr error // if set, SendMicrophonePCM returns this
}
// stashes handler so tests can dispatch directly; cancel just bumps a
// counter (handler stays reachable so tests can exercise closed-flag guards)
func (f *fakeNtg) OnFrame(h NtgFrameHandler) func() {
f.mu.Lock()
defer f.mu.Unlock()
f.handler = h
return func() {
f.mu.Lock()
defer f.mu.Unlock()
f.unregistered++
}
}
func (f *fakeNtg) SetExternalMicrophone(_ int64, capture bool, _ uint32, _ uint8) error {
f.mu.Lock()
defer f.mu.Unlock()
f.micCalls++
f.micCallCapture = append(f.micCallCapture, capture)
return nil
}
func (f *fakeNtg) ClearStreams(int64) error {
f.mu.Lock()
defer f.mu.Unlock()
f.clears++
return nil
}
func (f *fakeNtg) SendMicrophonePCM(_ int64, pcm []byte) error {
cp := make([]byte, len(pcm))
copy(cp, pcm)
f.mu.Lock()
defer f.mu.Unlock()
err := f.sendErr
f.sentPCM = append(f.sentPCM, cp)
return err
}
func (f *fakeNtg) sentCount() int {
f.mu.Lock()
defer f.mu.Unlock()
return len(f.sentPCM)
}
func newTestTgToXmpp(t *testing.T) (*TgToXmpp, *fakeNtg) {
t.Helper()
n := &fakeNtg{}
h, err := NewTgToXmpp(TgToXmppOptions{
Ntg: n,
ChatID: 12345,
})
if err != nil {
t.Fatalf("NewTgToXmpp: %v", err)
}
return h, n
}
func TestTgToXmppStartConfiguresPlayback(t *testing.T) {
h, n := newTestTgToXmpp(t)
if err := h.Start(); err != nil {
t.Fatalf("Start: %v", err)
}
defer h.Close()
n.mu.Lock()
defer n.mu.Unlock()
if n.micCalls != 1 {
t.Errorf("SetExternalMicrophone called %d times, want 1 (playback only)", n.micCalls)
}
if len(n.micCallCapture) != 1 || n.micCallCapture[0] {
t.Errorf("expected capture=false (playback) for tg->xmpp, got %v", n.micCallCapture)
}
}
func TestTgToXmppCloseStopsCallbackHandling(t *testing.T) {
h, n := newTestTgToXmpp(t)
if err := h.Start(); err != nil {
t.Fatalf("Start: %v", err)
}
if err := h.Close(); err != nil {
t.Fatalf("Close: %v", err)
}
// Post-close dispatches must be ignored.
frame := PCMFrame{SSRC: 1, Data: make([]byte, NtgFrameBytes)}
n.handler(h.opts.ChatID, []PCMFrame{frame, frame, frame, frame})
if got := len(h.acc); got != 0 {
t.Errorf("closed half accumulated %d bytes, want 0", got)
}
}
// mirrors ntgcalls' callback-per-goroutine dispatch; run with -race
func TestTgToXmppFeedFrameConcurrent(t *testing.T) {
h, _ := newTestTgToXmpp(t)
const N = 100
var wg sync.WaitGroup
wg.Add(N)
for i := 0; i < N; i++ {
go func() {
defer wg.Done()
f := PCMFrame{SSRC: 42, Data: make([]byte, NtgFrameBytes)}
h.feedFrame(f)
}()
}
wg.Wait()
h.mu.Lock()
defer h.mu.Unlock()
if got := len(h.acc); got != 0 && got != NtgFrameBytes {
t.Errorf("acc len=%d after %d concurrent feeds; want 0 or %d", got, N, NtgFrameBytes)
}
}