feat(test): up test coverage

This commit is contained in:
zarazaex69
2026-05-07 00:06:28 +03:00
parent 2907bf9a85
commit 9d8f063ce3
8 changed files with 1166 additions and 67 deletions
@@ -76,6 +76,12 @@ func TestCodecSpecsAndArgs(t *testing.T) {
if got := resolveEncoderCodec(vp8CodecSpec(), "none"); got != "libvpx" {
t.Fatalf("resolveEncoderCodec(vp8,none) = %q", got)
}
if got := resolveEncoderCodec(vp9CodecSpec(), "nvenc"); got != "vp9_nvenc" {
t.Fatalf("resolveEncoderCodec(vp9,nvenc) = %q", got)
}
if got := resolveEncoderCodec(codecSpec{mimeType: webrtc.MimeTypeAV1, encoder: "libaom-av1"}, "nvenc"); got != "av1_nvenc" {
t.Fatalf("resolveEncoderCodec(av1,nvenc) = %q", got)
}
args := buildEncoderArgs(vp8CodecSpec(), "vp8_nvenc", 320, 240, 30, "1M")
for _, want := range []string{"-video_size", "320x240", "-framerate", "30", "vp8_nvenc", "-b:v", "1M", "ivf"} {
@@ -87,6 +93,29 @@ func TestCodecSpecsAndArgs(t *testing.T) {
if h264Args[len(h264Args)-2] != "h264" {
t.Fatalf("h264 encoder args = %v", h264Args)
}
if got := resolveDecoderName(h264CodecSpec(), "nvenc"); got != "h264_cuvid" {
t.Fatalf("resolveDecoderName(h264,nvenc) = %q", got)
}
if got := resolveDecoderName(vp8CodecSpec(), "nvenc"); got != "vp8_cuvid" {
t.Fatalf("resolveDecoderName(vp8,nvenc) = %q", got)
}
if got := resolveDecoderName(vp9CodecSpec(), "nvenc"); got != "vp9_cuvid" {
t.Fatalf("resolveDecoderName(vp9,nvenc) = %q", got)
}
if got := resolveDecoderName(codecSpec{mimeType: "video/custom"}, "none"); got != "custom" {
t.Fatalf("resolveDecoderName(custom,none) = %q", got)
}
decArgs := buildDecoderArgs(vp8CodecSpec(), "vp8", 320, 240, "gray")
for _, want := range []string{"-f", "ivf", "-vcodec", "vp8", "scale=320:240:flags=neighbor,format=gray", "rawvideo"} {
if !slices.Contains(decArgs, want) {
t.Fatalf("buildDecoderArgs(vp8) = %v, missing %q", decArgs, want)
}
}
h264DecArgs := buildDecoderArgs(h264CodecSpec(), "h264", 320, 240, "gray")
if h264DecArgs[5] != "h264" {
t.Fatalf("buildDecoderArgs(h264) = %v", h264DecArgs)
}
}
type shortWriter struct {
@@ -105,6 +134,12 @@ type errWriter struct{}
func (w errWriter) Write([]byte) (int, error) { return 0, io.ErrClosedPipe }
type bufferWriteCloser struct {
bytes.Buffer
}
func (w *bufferWriteCloser) Close() error { return nil }
func TestIVFWritersAndWithStderr(t *testing.T) {
var buf bytes.Buffer
if err := writeIVFHeader(&buf, "VP80", 320, 240, 30); err != nil {
@@ -137,3 +172,83 @@ func TestIVFWritersAndWithStderr(t *testing.T) {
t.Fatalf("withStderr(nil) = %v", got)
}
}
func TestFFmpegProcessErrAndFrameValidation(t *testing.T) {
enc := &ffmpegEncoder{
stderr: bytes.NewBufferString("encoder failed"),
frames: make(chan []byte, 1),
frameSize: 4,
}
if _, err := enc.EncodeFrame([]byte("bad")); !errors.Is(err, ErrUnexpectedFrameSize) {
t.Fatalf("EncodeFrame(short) error = %v, want %v", err, ErrUnexpectedFrameSize)
}
enc.setErr(errors.New("boom"))
if _, err := enc.EncodeFrame([]byte("good")); err == nil || !strings.Contains(err.Error(), "encoder failed") {
t.Fatalf("EncodeFrame(processErr) error = %v", err)
}
dec := &ffmpegDecoder{stderr: bytes.NewBufferString("decoder failed")}
dec.setErr(errors.New("boom"))
if err := dec.PushSample([]byte("sample")); err == nil || !strings.Contains(err.Error(), "decoder failed") {
t.Fatalf("PushSample(processErr) error = %v", err)
}
closed := &ffmpegDecoder{}
closed.closed.Store(true)
if err := closed.processErr(); !errors.Is(err, ErrTransportClosed) {
t.Fatalf("decoder processErr(closed) = %v, want %v", err, ErrTransportClosed)
}
}
func TestFFmpegReadersAndSampleWriters(t *testing.T) {
var ivf bytes.Buffer
if err := writeIVFHeader(&ivf, "VP80", 2, 2, 30); err != nil {
t.Fatalf("writeIVFHeader() error = %v", err)
}
if err := writeIVFFrame(&ivf, 1, []byte("frame")); err != nil {
t.Fatalf("writeIVFFrame() error = %v", err)
}
enc := &ffmpegEncoder{stderr: &bytes.Buffer{}, frames: make(chan []byte, 2)}
enc.readIVF(&ivf)
if got := <-enc.frames; !bytes.Equal(got, []byte("frame")) {
t.Fatalf("readIVF frame = %q", got)
}
enc = &ffmpegEncoder{stderr: &bytes.Buffer{}, frames: make(chan []byte, 2)}
enc.readRawH264(bytes.NewBufferString("h264"))
if got := <-enc.frames; !bytes.Equal(got, []byte("h264")) {
t.Fatalf("readRawH264 frame = %q", got)
}
dec := &ffmpegDecoder{stderr: &bytes.Buffer{}, frames: make(chan []byte, 2), frameSize: 4}
dec.readRawFrames(bytes.NewBufferString("aaaabbbb"))
if got := <-dec.frames; !bytes.Equal(got, []byte("aaaa")) {
t.Fatalf("readRawFrames first = %q", got)
}
if got := <-dec.frames; !bytes.Equal(got, []byte("bbbb")) {
t.Fatalf("readRawFrames second = %q", got)
}
h264In := &bufferWriteCloser{}
dec = &ffmpegDecoder{stdin: h264In, mimeType: webrtc.MimeTypeH264}
if err := dec.PushSample([]byte("sample")); err != nil {
t.Fatalf("PushSample(h264) error = %v", err)
}
if h264In.String() != "sample" {
t.Fatalf("h264 stdin = %q", h264In.String())
}
ivfIn := &bufferWriteCloser{}
dec = &ffmpegDecoder{stdin: ivfIn, mimeType: webrtc.MimeTypeVP8}
if err := dec.PushSample([]byte("vp8")); err != nil {
t.Fatalf("PushSample(vp8) error = %v", err)
}
if ivfIn.Len() != 12+len("vp8") || dec.pts != 1 {
t.Fatalf("ivf stdin len=%d pts=%d", ivfIn.Len(), dec.pts)
}
dec = &ffmpegDecoder{frames: make(chan []byte, 1)}
dec.frames <- []byte("ready")
if got, err := dec.PopFrame(); err != nil || !bytes.Equal(got, []byte("ready")) {
t.Fatalf("PopFrame() = %q, %v", got, err)
}
}
@@ -0,0 +1,226 @@
package videochannel
import (
"context"
"errors"
"hash/crc32"
"testing"
"time"
"github.com/openlibrecommunity/olcrtc/internal/carrier"
"github.com/openlibrecommunity/olcrtc/internal/transport"
"github.com/pion/webrtc/v4"
)
type fakeVideoSession struct {
stream *fakeVideoStream
err error
}
func (s *fakeVideoSession) Capabilities() carrier.Capabilities {
return carrier.Capabilities{VideoTrack: true}
}
func (s *fakeVideoSession) OpenVideoTrack() (carrier.VideoTrack, error) {
if s.err != nil {
return nil, s.err
}
return s.stream, nil
}
type fakeVideoStream struct {
closeErr error
canSend bool
trackAdded bool
trackCB func(*webrtc.TrackRemote, *webrtc.RTPReceiver)
reconnect func()
should func() bool
ended func(string)
watched bool
closed bool
}
func (s *fakeVideoStream) Connect(context.Context) error { return nil }
func (s *fakeVideoStream) Close() error { s.closed = true; return s.closeErr }
func (s *fakeVideoStream) SetReconnectCallback(cb func()) { s.reconnect = cb }
func (s *fakeVideoStream) SetShouldReconnect(fn func() bool) { s.should = fn }
func (s *fakeVideoStream) SetEndedCallback(cb func(string)) { s.ended = cb }
func (s *fakeVideoStream) WatchConnection(context.Context) { s.watched = true }
func (s *fakeVideoStream) CanSend() bool { return s.canSend }
func (s *fakeVideoStream) AddTrack(webrtc.TrackLocal) error { s.trackAdded = true; return nil }
func (s *fakeVideoStream) SetTrackHandler(cb func(*webrtc.TrackRemote, *webrtc.RTPReceiver)) {
s.trackCB = cb
}
type nonVideoSession struct{}
func (s *nonVideoSession) Capabilities() carrier.Capabilities { return carrier.Capabilities{} }
func TestNewCallbacksFeaturesAndClose(t *testing.T) {
stream := &fakeVideoStream{canSend: true}
name := "videochannel-unit-new"
carrier.Register(name, func(context.Context, carrier.Config) (carrier.Session, error) {
return &fakeVideoSession{stream: stream}, nil
})
trIface, err := New(context.Background(), transport.Config{
Carrier: name,
VideoWidth: 320,
VideoHeight: 240,
VideoFPS: 30,
VideoBitrate: "1M",
VideoCodec: "qrcode",
VideoTileModule: -1,
VideoTileRS: -1,
})
if err != nil {
t.Fatalf("New() error = %v", err)
}
tr := trIface.(*streamTransport)
if !stream.trackAdded || stream.trackCB == nil {
t.Fatal("New() did not attach track and handler")
}
tr.SetReconnectCallback(func() {})
tr.SetShouldReconnect(func() bool { return true })
tr.SetEndedCallback(func(string) {})
tr.WatchConnection(context.Background())
if stream.reconnect == nil || stream.should == nil || stream.ended == nil || !stream.watched {
t.Fatal("callbacks/watch were not forwarded")
}
if !tr.CanSend() {
t.Fatal("CanSend() = false, want true")
}
if features := tr.Features(); !features.Reliable || !features.Ordered || !features.MessageOriented || features.MaxPayloadSize == 0 {
t.Fatalf("Features() = %+v", features)
}
if tr.videoQRSize != defaultFragmentSize || tr.videoTileModule != 4 || tr.videoTileRS != 20 {
t.Fatalf("defaults qr=%d tileModule=%d tileRS=%d", tr.videoQRSize, tr.videoTileModule, tr.videoTileRS)
}
if err := tr.Close(); err != nil {
t.Fatalf("Close() error = %v", err)
}
}
func TestNewErrorPaths(t *testing.T) {
carrier.Register("videochannel-create-fails", func(context.Context, carrier.Config) (carrier.Session, error) {
return nil, errors.New("boom")
})
if _, err := New(context.Background(), transport.Config{Carrier: "videochannel-create-fails"}); err == nil || err.Error() != "create provider transport: boom" {
t.Fatalf("New() error = %v", err)
}
carrier.Register("videochannel-no-video", func(context.Context, carrier.Config) (carrier.Session, error) {
return &nonVideoSession{}, nil
})
if _, err := New(context.Background(), transport.Config{Carrier: "videochannel-no-video"}); !errors.Is(err, ErrVideoTrackUnsupported) {
t.Fatalf("New() error = %v, want %v", err, ErrVideoTrackUnsupported)
}
carrier.Register("videochannel-open-fails", func(context.Context, carrier.Config) (carrier.Session, error) {
return &fakeVideoSession{err: errors.New("open boom")}, nil
})
if _, err := New(context.Background(), transport.Config{Carrier: "videochannel-open-fails"}); err == nil || err.Error() != "open video track: open boom" {
t.Fatalf("New() error = %v", err)
}
}
func TestSendAckAndClosePaths(t *testing.T) {
tr := &streamTransport{
stream: &fakeVideoStream{canSend: true},
outbound: make(chan []byte, 8),
outboundAck: make(chan []byte, 8),
closeCh: make(chan struct{}),
writerDone: make(chan struct{}),
ackWaiters: make(map[uint32]chan uint32),
videoQRSize: 4,
}
done := make(chan error, 1)
payload := []byte("payload")
go func() { done <- tr.Send(payload) }()
select {
case frame := <-tr.outbound:
decoded, err := decodeTransportFrame(frame)
if err != nil {
t.Fatalf("decodeTransportFrame() error = %v", err)
}
tr.resolveAck(decoded.seq, crc32.ChecksumIEEE(payload))
case <-time.After(time.Second):
t.Fatal("Send() did not enqueue frame")
}
if err := <-done; err != nil {
t.Fatalf("Send() error = %v", err)
}
if err := tr.Close(); err != nil {
t.Fatalf("Close() error = %v", err)
}
if err := tr.Send([]byte("closed")); !errors.Is(err, ErrTransportClosed) {
t.Fatalf("Send(closed) error = %v, want %v", err, ErrTransportClosed)
}
}
func TestOutboundPriorityRenderAndClosedEnqueue(t *testing.T) {
tr := &streamTransport{
stream: &fakeVideoStream{canSend: true},
outbound: make(chan []byte, 2),
outboundAck: make(chan []byte, 2),
closeCh: make(chan struct{}),
writerDone: make(chan struct{}),
videoW: 16,
videoH: 16,
videoQRRecovery: "highest",
videoCodec: "qrcode",
videoTileModule: 4,
videoTileRS: 20,
}
if err := tr.enqueueFrame([]byte("data"), false); err != nil {
t.Fatalf("enqueueFrame(data) error = %v", err)
}
if err := tr.enqueueFrame([]byte("ack"), true); err != nil {
t.Fatalf("enqueueFrame(ack) error = %v", err)
}
if got, ok := tr.nextOutboundFrame(); !ok || string(got) != "ack" {
t.Fatalf("first nextOutboundFrame() = %q/%v, want ack/true", got, ok)
}
if got, ok := tr.nextOutboundFrame(); !ok || string(got) != "data" {
t.Fatalf("second nextOutboundFrame() = %q/%v, want data/true", got, ok)
}
if got, ok := tr.nextOutboundFrame(); !ok || got != nil {
t.Fatalf("idle nextOutboundFrame() = %q/%v, want nil/true", got, ok)
}
idle, err := tr.renderFrame(nil)
if err != nil {
t.Fatalf("renderFrame(nil) error = %v", err)
}
if len(idle) != tr.videoW*tr.videoH {
t.Fatalf("idle frame len = %d, want %d", len(idle), tr.videoW*tr.videoH)
}
if features := tr.Features(); features.MaxPayloadSize != defaultMaxPayloadSize {
t.Fatalf("Features() = %+v", features)
}
tr.videoQRSize = defaultMaxPayloadSize
if features := tr.Features(); features.MaxPayloadSize <= defaultMaxPayloadSize {
t.Fatalf("Features(large qr) = %+v", features)
}
tr.closed.Store(true)
if err := tr.enqueueFrame([]byte("closed"), false); !errors.Is(err, ErrTransportClosed) {
t.Fatalf("enqueueFrame(closed) error = %v, want %v", err, ErrTransportClosed)
}
}
func TestNextOutboundFrameStopsWhenClosed(t *testing.T) {
tr := &streamTransport{
outbound: make(chan []byte, 1),
outboundAck: make(chan []byte, 1),
closeCh: make(chan struct{}),
}
close(tr.closeCh)
if got, ok := tr.nextOutboundFrame(); ok || got != nil {
t.Fatalf("nextOutboundFrame(closed) = %q/%v, want nil/false", got, ok)
}
}