package datachannel import ( "context" "testing" "time" "go.uber.org/atomic" "github.com/pion/datachannel" "github.com/pion/transport/v4/deadline" "github.com/stretchr/testify/require" ) func TestDataChannelWriter(t *testing.T) { mockDC := newMockDataChannelWriter() // slow threshold is 1000B/s w := NewDataChannelWriterReliable(mockDC, mockDC, 8000) require.Equal(t, mockDC, w.BufferedAmountGetter()) buf := make([]byte, 2000) // write 2000 bytes so it should not drop in 2 seconds t0 := time.Now() n, err := w.Write(buf) require.NoError(t, err) require.Equal(t, 2000, n) t1 := time.Now() mockDC.SetNextWriteCompleteAt(t0.Add(time.Second * 3 / 2)) n, err = w.Write(buf[:10]) require.NoError(t, err) require.Equal(t, 10, n) require.GreaterOrEqual(t, time.Since(t1), time.Second) // bitrate below slow threshold(2000bytes/3sec), should drop by timeout mockDC.SetNextWriteCompleteAt(t0.Add(3 * time.Second)) n, err = w.Write(buf[:1000]) require.ErrorIs(t, err, ErrDataDroppedBySlowReader, err) require.Equal(t, 0, n) } func TestDataChannelWriter_NoSlowThreshold(t *testing.T) { mockDC := newMockDataChannelWriter() w := NewDataChannelWriterReliable(mockDC, mockDC, 0) buf := make([]byte, 2000) n, err := w.Write(buf) require.NoError(t, err) require.Equal(t, 2000, n) mockDC.SetNextWriteCompleteAt(time.Now().Add(singleWriteTimeout / 2)) n, err = w.Write(buf[:10]) require.NoError(t, err) require.Equal(t, 10, n) // slow threshold is 0, should not block & retry mockDC.SetNextWriteCompleteAt(time.Now().Add(singleWriteTimeout * 2)) n, err = w.Write(buf[:1000]) require.ErrorIs(t, err, context.DeadlineExceeded, err) require.Equal(t, 0, n) } func TestDataChannelWriter_Unreliable(t *testing.T) { mockDC := newMockLossyDataChannelWriter(8192) w := NewDataChannelWriterUnreliable(mockDC, mockDC, 100*time.Millisecond, 2000) for range 10 { buf := make([]byte, 128) _, err := w.Write(buf) require.NoError(t, err) time.Sleep(100 * time.Millisecond) } buf := make([]byte, 4096) _, err := w.Write(buf) require.NoError(t, err) // should drop due to high buffered amount _, err = w.Write(buf) require.ErrorIs(t, err, ErrDataDroppedByHighBufferedAmount) } // mockDataChannelWriter type mockDataChannelWriter struct { datachannel.ReadWriteCloserDeadliner nextWriteCompleteAt time.Time deadline *deadline.Deadline buffered atomic.Int64 } func newMockDataChannelWriter() *mockDataChannelWriter { return &mockDataChannelWriter{ deadline: deadline.New(), } } func (m *mockDataChannelWriter) BufferedAmount() uint64 { return uint64(m.buffered.Load()) } func (m *mockDataChannelWriter) Write(b []byte) (int, error) { // buffered while the write is in flight, cleared once it completes m.buffered.Store(int64(len(b))) wait := time.Until(m.nextWriteCompleteAt) if wait <= 0 { m.buffered.Store(0) return len(b), nil } select { case <-m.deadline.Done(): return 0, m.deadline.Err() case <-time.After(wait): m.buffered.Store(0) return len(b), nil } } func (m *mockDataChannelWriter) SetWriteDeadline(t time.Time) error { m.deadline.Set(t) return nil } func (m *mockDataChannelWriter) SetNextWriteCompleteAt(t time.Time) { m.nextWriteCompleteAt = t } // mockLossyDataChannelWriter type mockLossyDataChannelWriter struct { datachannel.ReadWriteCloserDeadliner bufferedAmount atomic.Int64 targetBitrate int lastWriteAt time.Time } func newMockLossyDataChannelWriter(targetBitrate int) *mockLossyDataChannelWriter { return &mockLossyDataChannelWriter{ targetBitrate: targetBitrate, lastWriteAt: time.Now(), } } func (m *mockLossyDataChannelWriter) BufferedAmount() uint64 { return uint64(m.bufferedAmount.Load()) } func (m *mockLossyDataChannelWriter) Write(b []byte) (int, error) { m.bufferedAmount.Add(int64(len(b))) if time.Now().Before(m.lastWriteAt) { return len(b), nil } // drain buffer based on target bitrate canWriteBytes := time.Since(m.lastWriteAt) * time.Duration(m.targetBitrate) / time.Second / 8 if m.bufferedAmount.Load() <= int64(canWriteBytes) { m.lastWriteAt = m.lastWriteAt.Add(time.Duration(int64(time.Second) * int64(m.BufferedAmount()) / (int64(m.targetBitrate) / 8))) m.bufferedAmount.Store(0) } else { m.lastWriteAt = time.Now() m.bufferedAmount.Add(-int64(canWriteBytes)) } return len(b), nil } func (m *mockLossyDataChannelWriter) SetWriteDeadline(t time.Time) error { return nil }