Repository: quic-go/quic-go
Pull request #5084 (2025-04-28 04:27:57)

ackhandler: migrate the ECN tests away from Ginkgo
Part of #3652. This PR concludes the conversion of the `ackhandler` tests.

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/quic-go/quic-go/pull/5084?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=quic-go) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 83.94%. Comparing base [(`597fee0`)](https://app.codecov.io/gh/quic-go/quic-go/commit/597fee0b3442304f44f8a2e7a77623acc18b3e38?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=quic-go) to head [(`4b4aa2d`)](https://app.codecov.io/gh/quic-go/quic-go/commit/4b4aa2de1a3ac2a6321a5ea7cb95679a8eaa8428?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=quic-go).

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #5084      +/-   ##
==========================================
- Coverage   84.12%   83.94%   -0.18%     
==========================================
  Files         153      150       -3     
  Lines       16994    16649     -345     
==========================================
- Hits        14296    13976     -320     
+ Misses       2165     2153      -12     
+ Partials      533      520      -13     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/quic-go/quic-go/pull/5084?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=quic-go).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=quic-go).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>

Patch:
diff --git a/connection.go b/connection.go
index 18bb6e03..0ed4e1f0 100644
--- a/connection.go
+++ b/connection.go
@@ -32,13 +32,13 @@ type unpacker interface {
 
 type streamManager interface {
 	GetOrOpenSendStream(protocol.StreamID) (*SendStream, error)
-	GetOrOpenReceiveStream(protocol.StreamID) (receiveStreamI, error)
+	GetOrOpenReceiveStream(protocol.StreamID) (*ReceiveStream, error)
 	OpenStream() (*Stream, error)
 	OpenUniStream() (*SendStream, error)
 	OpenStreamSync(context.Context) (*Stream, error)
 	OpenUniStreamSync(context.Context) (*SendStream, error)
 	AcceptStream(context.Context) (*Stream, error)
-	AcceptUniStream(context.Context) (ReceiveStream, error)
+	AcceptUniStream(context.Context) (*ReceiveStream, error)
 	DeleteStream(protocol.StreamID) error
 	UpdateLimits(*wire.TransportParameters)
 	HandleMaxStreamsFrame(*wire.MaxStreamsFrame)
@@ -2496,7 +2496,7 @@ func (s *connection) AcceptStream(ctx context.Context) (*Stream, error) {
 	return s.streamsMap.AcceptStream(ctx)
 }
 
-func (s *connection) AcceptUniStream(ctx context.Context) (ReceiveStream, error) {
+func (s *connection) AcceptUniStream(ctx context.Context) (*ReceiveStream, error) {
 	return s.streamsMap.AcceptUniStream(ctx)
 }
 
diff --git a/http3/client.go b/http3/client.go
index 6bd880cf..d9666bb7 100644
--- a/http3/client.go
+++ b/http3/client.go
@@ -84,7 +84,7 @@ func newClientConn(
 	enableDatagrams bool,
 	additionalSettings map[uint64]uint64,
 	streamHijacker func(FrameType, quic.ConnectionTracingID, *quic.Stream, error) (hijacked bool, err error),
-	uniStreamHijacker func(StreamType, quic.ConnectionTracingID, quic.ReceiveStream, error) (hijacked bool),
+	uniStreamHijacker func(StreamType, quic.ConnectionTracingID, *quic.ReceiveStream, error) (hijacked bool),
 	maxResponseHeaderBytes int64,
 	disableCompression bool,
 	logger *slog.Logger,
diff --git a/http3/conn.go b/http3/conn.go
index 4ef53e87..61a83745 100644
--- a/http3/conn.go
+++ b/http3/conn.go
@@ -209,7 +209,7 @@ func (c *connection) CloseWithError(code quic.ApplicationErrorCode, msg string)
 	return c.Connection.CloseWithError(code, msg)
 }
 
-func (c *connection) handleUnidirectionalStreams(hijack func(StreamType, quic.ConnectionTracingID, quic.ReceiveStream, error) (hijacked bool)) {
+func (c *connection) handleUnidirectionalStreams(hijack func(StreamType, quic.ConnectionTracingID, *quic.ReceiveStream, error) (hijacked bool)) {
 	var (
 		rcvdControlStr      atomic.Bool
 		rcvdQPACKEncoderStr atomic.Bool
@@ -225,7 +225,7 @@ func (c *connection) handleUnidirectionalStreams(hijack func(StreamType, quic.Co
 			return
 		}
 
-		go func(str quic.ReceiveStream) {
+		go func(str *quic.ReceiveStream) {
 			streamType, err := quicvarint.Read(quicvarint.NewReader(str))
 			if err != nil {
 				id := c.Context().Value(quic.ConnectionTracingKey).(quic.ConnectionTracingID)
@@ -286,7 +286,7 @@ func (c *connection) handleUnidirectionalStreams(hijack func(StreamType, quic.Co
 	}
 }
 
-func (c *connection) handleControlStream(str quic.ReceiveStream) {
+func (c *connection) handleControlStream(str *quic.ReceiveStream) {
 	fp := &frameParser{conn: c.Connection, r: str}
 	f, err := fp.ParseNext()
 	if err != nil {
diff --git a/http3/server.go b/http3/server.go
index fc1ebede..85f1356b 100644
--- a/http3/server.go
+++ b/http3/server.go
@@ -161,7 +161,7 @@ type Server struct {
 	// UniStreamHijacker, when set, is called for unknown unidirectional stream of unknown stream type.
 	// If parsing the stream type fails, the error is passed to the callback.
 	// In that case, the stream type will not be set.
-	UniStreamHijacker func(StreamType, quic.ConnectionTracingID, quic.ReceiveStream, error) (hijacked bool)
+	UniStreamHijacker func(StreamType, quic.ConnectionTracingID, *quic.ReceiveStream, error) (hijacked bool)
 
 	// IdleTimeout specifies how long until idle clients connection should be
 	// closed. Idle refers only to the HTTP/3 layer, activity at the QUIC layer
diff --git a/http3/transport.go b/http3/transport.go
index 042cded2..f109a154 100644
--- a/http3/transport.go
+++ b/http3/transport.go
@@ -99,7 +99,7 @@ type Transport struct {
 	DisableCompression bool
 
 	StreamHijacker    func(FrameType, quic.ConnectionTracingID, *quic.Stream, error) (hijacked bool, err error)
-	UniStreamHijacker func(StreamType, quic.ConnectionTracingID, quic.ReceiveStream, error) (hijacked bool)
+	UniStreamHijacker func(StreamType, quic.ConnectionTracingID, *quic.ReceiveStream, error) (hijacked bool)
 
 	Logger *slog.Logger
 
diff --git a/interface.go b/interface.go
index 9fb81124..1912c272 100644
--- a/interface.go
+++ b/interface.go
@@ -4,7 +4,6 @@ import (
 	"context"
 	"crypto/tls"
 	"errors"
-	"io"
 	"net"
 	"time"
 
@@ -70,25 +69,6 @@ type connTracingCtxKey struct{}
 // context returned by tls.Config.ClientInfo.Context.
 var QUICVersionContextKey = handshake.QUICVersionContextKey
 
-// A ReceiveStream is a unidirectional Receive Stream.
-type ReceiveStream interface {
-	// StreamID returns the stream ID.
-	StreamID() StreamID
-	// Read reads data from the stream.
-	// Read can be made to time out using SetDeadline and SetReadDeadline.
-	// If the stream was canceled, the error is a StreamError.
-	io.Reader
-	// CancelRead aborts receiving on this stream.
-	// It will ask the peer to stop transmitting stream data.
-	// Read will unblock immediately, and future Read calls will fail.
-	// When called multiple times or after reading the io.EOF it is a no-op.
-	CancelRead(StreamErrorCode)
-	// SetReadDeadline sets the deadline for future Read calls and
-	// any currently-blocked Read call.
-	// A zero value for t means Read will not time out.
-	SetReadDeadline(t time.Time) error
-}
-
 // A Connection is a QUIC connection between two peers.
 // Calls to the connection (and to streams) can return the following types of errors:
 //   - [ApplicationError]: for errors triggered by the application running on top of QUIC
@@ -101,7 +81,7 @@ type Connection interface {
 	// AcceptStream returns the next stream opened by the peer, blocking until one is available.
 	AcceptStream(context.Context) (*Stream, error)
 	// AcceptUniStream returns the next unidirectional stream opened by the peer, blocking until one is available.
-	AcceptUniStream(context.Context) (ReceiveStream, error)
+	AcceptUniStream(context.Context) (*ReceiveStream, error)
 	// OpenStream opens a new bidirectional QUIC stream.
 	// There is no signaling to the peer about new streams:
 	// The peer can only accept the stream after data has been sent on the stream,
diff --git a/mockgen.go b/mockgen.go
index aeb6094a..4384f5d2 100644
--- a/mockgen.go
+++ b/mockgen.go
@@ -11,9 +11,6 @@ type RawConn = rawConn
 //go:generate sh -c "go run go.uber.org/mock/mockgen -typed -build_flags=\"-tags=gomock\" -package quic -self_package github.com/quic-go/quic-go -destination mock_sender_test.go github.com/quic-go/quic-go Sender"
 type Sender = sender
 
-//go:generate sh -c "go run go.uber.org/mock/mockgen -typed -build_flags=\"-tags=gomock\" -package quic -self_package github.com/quic-go/quic-go -destination mock_receive_stream_internal_test.go github.com/quic-go/quic-go ReceiveStreamI"
-type ReceiveStreamI = receiveStreamI
-
 //go:generate sh -c "go run go.uber.org/mock/mockgen -typed -build_flags=\"-tags=gomock\" -package quic -self_package github.com/quic-go/quic-go -destination mock_stream_sender_test.go github.com/quic-go/quic-go StreamSender"
 type StreamSender = streamSender
 
diff --git a/receive_stream.go b/receive_stream.go
index 192b92f7..246bdac4 100644
--- a/receive_stream.go
+++ b/receive_stream.go
@@ -14,15 +14,8 @@ import (
 	"github.com/quic-go/quic-go/internal/wire"
 )
 
-type receiveStreamI interface {
-	ReceiveStream
-
-	handleStreamFrame(*wire.StreamFrame, time.Time) error
-	handleResetStreamFrame(*wire.ResetStreamFrame, time.Time) error
-	closeForShutdown(error)
-}
-
-type receiveStream struct {
+// A ReceiveStream is a unidirectional Receive Stream.
+type ReceiveStream struct {
 	mutex sync.Mutex
 
 	streamID protocol.StreamID
@@ -56,18 +49,14 @@ type receiveStream struct {
 	flowController flowcontrol.StreamFlowController
 }
 
-var (
-	_ ReceiveStream            = &receiveStream{}
-	_ receiveStreamI           = &receiveStream{}
-	_ streamControlFrameGetter = &receiveStream{}
-)
+var _ streamControlFrameGetter = &ReceiveStream{}
 
 func newReceiveStream(
 	streamID protocol.StreamID,
 	sender streamSender,
 	flowController flowcontrol.StreamFlowController,
-) *receiveStream {
-	return &receiveStream{
+) *ReceiveStream {
+	return &ReceiveStream{
 		streamID:       streamID,
 		sender:         sender,
 		flowController: flowController,
@@ -78,12 +67,15 @@ func newReceiveStream(
 	}
 }
 
-func (s *receiveStream) StreamID() protocol.StreamID {
+// StreamID returns the stream ID.
+func (s *ReceiveStream) StreamID() protocol.StreamID {
 	return s.streamID
 }
 
-// Read implements io.Reader. It is not thread safe!
-func (s *receiveStream) Read(p []byte) (int, error) {
+// Read reads data from the stream.
+// Read can be made to time out using SetDeadline and SetReadDeadline.
+// If the stream was canceled, the error is a StreamError.
+func (s *ReceiveStream) Read(p []byte) (int, error) {
 	// Concurrent use of Read is not permitted (and doesn't make any sense),
 	// but sometimes people do it anyway.
 	// Make sure that we only execute one call at any given time to avoid hard to debug failures.
@@ -107,7 +99,7 @@ func (s *receiveStream) Read(p []byte) (int, error) {
 	return n, err
 }
 
-func (s *receiveStream) isNewlyCompleted() bool {
+func (s *ReceiveStream) isNewlyCompleted() bool {
 	if s.completed {
 		return false
 	}
@@ -128,7 +120,7 @@ func (s *receiveStream) isNewlyCompleted() bool {
 	return false
 }
 
-func (s *receiveStream) readImpl(p []byte) (hasStreamWindowUpdate bool, hasConnWindowUpdate bool, _ int, _ error) {
+func (s *ReceiveStream) readImpl(p []byte) (hasStreamWindowUpdate bool, hasConnWindowUpdate bool, _ int, _ error) {
 	if s.currentFrameIsLast && s.currentFrame == nil {
 		s.errorRead = true
 		return false, false, 0, io.EOF
@@ -229,7 +221,7 @@ func (s *receiveStream) readImpl(p []byte) (hasStreamWindowUpdate bool, hasConnW
 	return hasStreamWindowUpdate, hasConnWindowUpdate, bytesRead, nil
 }
 
-func (s *receiveStream) dequeueNextFrame() {
+func (s *ReceiveStream) dequeueNextFrame() {
 	var offset protocol.ByteCount
 	// We're done with the last frame. Release the buffer.
 	if s.currentFrameDone != nil {
@@ -240,7 +232,11 @@ func (s *receiveStream) dequeueNextFrame() {
 	s.readPosInFrame = 0
 }
 
-func (s *receiveStream) CancelRead(errorCode StreamErrorCode) {
+// CancelRead aborts receiving on this stream.
+// It will ask the peer to stop transmitting stream data.
+// Read will unblock immediately, and future Read calls will fail.
+// When called multiple times or after reading the io.EOF it is a no-op.
+func (s *ReceiveStream) CancelRead(errorCode StreamErrorCode) {
 	s.mutex.Lock()
 	queuedNewControlFrame := s.cancelReadImpl(errorCode)
 	completed := s.isNewlyCompleted()
@@ -255,7 +251,7 @@ func (s *receiveStream) CancelRead(errorCode StreamErrorCode) {
 	}
 }
 
-func (s *receiveStream) cancelReadImpl(errorCode qerr.StreamErrorCode) (queuedNewControlFrame bool) {
+func (s *ReceiveStream) cancelReadImpl(errorCode qerr.StreamErrorCode) (queuedNewControlFrame bool) {
 	if s.cancelledLocally { // duplicate call to CancelRead
 		return false
 	}
@@ -272,7 +268,7 @@ func (s *receiveStream) cancelReadImpl(errorCode qerr.StreamErrorCode) (queuedNe
 	return true
 }
 
-func (s *receiveStream) handleStreamFrame(frame *wire.StreamFrame, now time.Time) error {
+func (s *ReceiveStream) handleStreamFrame(frame *wire.StreamFrame, now time.Time) error {
 	s.mutex.Lock()
 	err := s.handleStreamFrameImpl(frame, now)
 	completed := s.isNewlyCompleted()
@@ -285,7 +281,7 @@ func (s *receiveStream) handleStreamFrame(frame *wire.StreamFrame, now time.Time
 	return err
 }
 
-func (s *receiveStream) handleStreamFrameImpl(frame *wire.StreamFrame, now time.Time) error {
+func (s *ReceiveStream) handleStreamFrameImpl(frame *wire.StreamFrame, now time.Time) error {
 	maxOffset := frame.Offset + frame.DataLen()
 	if err := s.flowController.UpdateHighestReceived(maxOffset, frame.Fin, now); err != nil {
 		return err
@@ -303,7 +299,7 @@ func (s *receiveStream) handleStreamFrameImpl(frame *wire.StreamFrame, now time.
 	return nil
 }
 
-func (s *receiveStream) handleResetStreamFrame(frame *wire.ResetStreamFrame, now time.Time) error {
+func (s *ReceiveStream) handleResetStreamFrame(frame *wire.ResetStreamFrame, now time.Time) error {
 	s.mutex.Lock()
 	err := s.handleResetStreamFrameImpl(frame, now)
 	completed := s.isNewlyCompleted()
@@ -315,7 +311,7 @@ func (s *receiveStream) handleResetStreamFrame(frame *wire.ResetStreamFrame, now
 	return err
 }
 
-func (s *receiveStream) handleResetStreamFrameImpl(frame *wire.ResetStreamFrame, now time.Time) error {
+func (s *ReceiveStream) handleResetStreamFrameImpl(frame *wire.ResetStreamFrame, now time.Time) error {
 	if s.closeForShutdownErr != nil {
 		return nil
 	}
@@ -339,7 +335,7 @@ func (s *receiveStream) handleResetStreamFrameImpl(frame *wire.ResetStreamFrame,
 	return nil
 }
 
-func (s *receiveStream) getControlFrame(now time.Time) (_ ackhandler.Frame, ok, hasMore bool) {
+func (s *ReceiveStream) getControlFrame(now time.Time) (_ ackhandler.Frame, ok, hasMore bool) {
 	s.mutex.Lock()
 	defer s.mutex.Unlock()
 
@@ -362,7 +358,10 @@ func (s *receiveStream) getControlFrame(now time.Time) (_ ackhandler.Frame, ok,
 	}, true, false
 }
 
-func (s *receiveStream) SetReadDeadline(t time.Time) error {
+// SetReadDeadline sets the deadline for future Read calls and
+// any currently-blocked Read call.
+// A zero value for t means Read will not time out.
+func (s *ReceiveStream) SetReadDeadline(t time.Time) error {
 	s.mutex.Lock()
 	s.deadline = t
 	s.mutex.Unlock()
@@ -373,7 +372,7 @@ func (s *receiveStream) SetReadDeadline(t time.Time) error {
 // CloseForShutdown closes a stream abruptly.
 // It makes Read unblock (and return the error) immediately.
 // The peer will NOT be informed about this: the stream is closed without sending a FIN or RESET.
-func (s *receiveStream) closeForShutdown(err error) {
+func (s *ReceiveStream) closeForShutdown(err error) {
 	s.mutex.Lock()
 	s.closeForShutdownErr = err
 	s.mutex.Unlock()
@@ -381,7 +380,7 @@ func (s *receiveStream) closeForShutdown(err error) {
 }
 
 // signalRead performs a non-blocking send on the readChan
-func (s *receiveStream) signalRead() {
+func (s *ReceiveStream) signalRead() {
 	select {
 	case s.readChan <- struct{}{}:
 	default:
diff --git a/stream.go b/stream.go
index 85b70bb2..9b18b930 100644
--- a/stream.go
+++ b/stream.go
@@ -49,7 +49,7 @@ func (s *uniStreamSender) onHasStreamControlFrame(id protocol.StreamID, str stre
 var _ streamSender = &uniStreamSender{}
 
 type Stream struct {
-	receiveStream
+	*ReceiveStream
 	*SendStream
 
 	completedMutex         sync.Mutex
@@ -58,8 +58,6 @@ type Stream struct {
 	sendStreamCompleted    bool
 }
 
-var _ streamControlFrameGetter = &receiveStream{}
-
 // newStream creates a new Stream
 func newStream(
 	ctx context.Context,
@@ -93,7 +91,7 @@ func newStream(
 			sender.onHasStreamControlFrame(streamID, s)
 		},
 	}
-	s.receiveStream = *newReceiveStream(streamID, senderForReceiveStream, flowController)
+	s.ReceiveStream = newReceiveStream(streamID, senderForReceiveStream, flowController)
 	return s
 }
 
@@ -112,7 +110,7 @@ func (s *Stream) getControlFrame(now time.Time) (_ ackhandler.Frame, ok, hasMore
 	if ok {
 		return f, true, true
 	}
-	return s.receiveStream.getControlFrame(now)
+	return s.ReceiveStream.getControlFrame(now)
 }
 
 func (s *Stream) SetDeadline(t time.Time) error {
@@ -126,7 +124,7 @@ func (s *Stream) SetDeadline(t time.Time) error {
 // The peer will NOT be informed about this: the stream is closed without sending a FIN or RST.
 func (s *Stream) closeForShutdown(err error) {
 	s.SendStream.closeForShutdown(err)
-	s.receiveStream.closeForShutdown(err)
+	s.ReceiveStream.closeForShutdown(err)
 }
 
 // checkIfCompleted is called from the uniStreamSender, when one of the stream halves is completed.
diff --git a/streams_map.go b/streams_map.go
index 0f2f01ff..dfc0e901 100644
--- a/streams_map.go
+++ b/streams_map.go
@@ -54,7 +54,7 @@ type streamsMap struct {
 	outgoingBidiStreams *outgoingStreamsMap[*Stream]
 	outgoingUniStreams  *outgoingStreamsMap[*SendStream]
 	incomingBidiStreams *incomingStreamsMap[*Stream]
-	incomingUniStreams  *incomingStreamsMap[receiveStreamI]
+	incomingUniStreams  *incomingStreamsMap[*ReceiveStream]
 	reset               bool
 }
 
@@ -110,7 +110,7 @@ func (m *streamsMap) initMaps() {
 	)
 	m.incomingUniStreams = newIncomingStreamsMap(
 		protocol.StreamTypeUni,
-		func(num protocol.StreamNum) receiveStreamI {
+		func(num protocol.StreamNum) *ReceiveStream {
 			id := num.StreamID(protocol.StreamTypeUni, m.perspective.Opposite())
 			return newReceiveStream(id, m.sender, m.newFlowController(id))
 		},
@@ -179,7 +179,7 @@ func (m *streamsMap) AcceptStream(ctx context.Context) (*Stream, error) {
 	return str, convertStreamError(err, protocol.StreamTypeBidi, m.perspective.Opposite())
 }
 
-func (m *streamsMap) AcceptUniStream(ctx context.Context) (ReceiveStream, error) {
+func (m *streamsMap) AcceptUniStream(ctx context.Context) (*ReceiveStream, error) {
 	m.mutex.Lock()
 	reset := m.reset
 	mm := m.incomingUniStreams
@@ -208,7 +208,7 @@ func (m *streamsMap) DeleteStream(id protocol.StreamID) error {
 	panic("")
 }
 
-func (m *streamsMap) GetOrOpenReceiveStream(id protocol.StreamID) (receiveStreamI, error) {
+func (m *streamsMap) GetOrOpenReceiveStream(id protocol.StreamID) (*ReceiveStream, error) {
 	str, err := m.getOrOpenReceiveStream(id)
 	if err != nil {
 		return nil, &qerr.TransportError{
@@ -219,7 +219,7 @@ func (m *streamsMap) GetOrOpenReceiveStream(id protocol.StreamID) (receiveStream
 	return str, nil
 }
 
-func (m *streamsMap) getOrOpenReceiveStream(id protocol.StreamID) (receiveStreamI, error) {
+func (m *streamsMap) getOrOpenReceiveStream(id protocol.StreamID) (*ReceiveStream, error) {
 	num := id.StreamNum()
 	switch id.Type() {
 	case protocol.StreamTypeUni:
@@ -235,13 +235,13 @@ func (m *streamsMap) getOrOpenReceiveStream(id protocol.StreamID) (receiveStream
 			if str == nil && err == nil {
 				return nil, nil
 			}
-			return str, convertStreamError(err, protocol.StreamTypeBidi, id.InitiatedBy())
+			return str.ReceiveStream, convertStreamError(err, protocol.StreamTypeBidi, id.InitiatedBy())
 		} else {
 			str, err := m.incomingBidiStreams.GetOrOpenStream(num)
 			if str == nil && err == nil {
 				return nil, nil
 			}
-			return str, convertStreamError(err, protocol.StreamTypeBidi, id.InitiatedBy())
+			return str.ReceiveStream, convertStreamError(err, protocol.StreamTypeBidi, id.InitiatedBy())
 		}
 	}
 	panic("")


Test patch:
diff --git a/connection_test.go b/connection_test.go
index 1dcfa8a4..2812e26e 100644
--- a/connection_test.go
+++ b/connection_test.go
@@ -219,15 +219,19 @@ func TestConnectionHandleReceiveStreamFrames(t *testing.T) {
 		mockCtrl := gomock.NewController(t)
 		streamsMap := NewMockStreamManager(mockCtrl)
 		tc := newServerTestConnection(t, mockCtrl, nil, false, connectionOptStreamManager(streamsMap))
-		str := NewMockReceiveStreamI(mockCtrl)
+		mockSender := NewMockStreamSender(mockCtrl)
+		mockSender.EXPECT().onHasStreamData(streamID, gomock.Any()).AnyTimes()
+		mockFC := mocks.NewMockStreamFlowController(mockCtrl)
+		str := newReceiveStream(streamID, mockSender, mockFC)
 		// STREAM frame
+		mockFC.EXPECT().UpdateHighestReceived(protocol.ByteCount(len(f.Data)), false, gomock.Any())
 		streamsMap.EXPECT().GetOrOpenReceiveStream(streamID).Return(str, nil)
-		str.EXPECT().handleStreamFrame(f, now)
 		_, err := tc.conn.handleFrame(f, protocol.Encryption1RTT, connID, now)
 		require.NoError(t, err)
 		// RESET_STREAM frame
+		mockFC.EXPECT().UpdateHighestReceived(protocol.ByteCount(1337), true, gomock.Any())
+		mockFC.EXPECT().Abandon()
 		streamsMap.EXPECT().GetOrOpenReceiveStream(streamID).Return(str, nil)
-		str.EXPECT().handleResetStreamFrame(rsf, now)
 		_, err = tc.conn.handleFrame(rsf, protocol.Encryption1RTT, connID, now)
 		require.NoError(t, err)
 		// STREAM_DATA_BLOCKED frames are not passed to the stream
@@ -399,17 +403,18 @@ func TestConnectionAcceptStreams(t *testing.T) {
 	// bidirectional streams
 	ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
 	defer cancel()
-	mstr := &Stream{}
-	streamsMap.EXPECT().AcceptStream(ctx).Return(mstr, nil)
+	str1 := &Stream{}
+	streamsMap.EXPECT().AcceptStream(ctx).Return(str1, nil)
 	str, err := tc.conn.AcceptStream(ctx)
 	require.NoError(t, err)
-	require.Equal(t, mstr, str)
+	require.Equal(t, str1, str)
 
 	// unidirectional streams
-	streamsMap.EXPECT().AcceptUniStream(ctx).Return(mstr, nil)
+	str2 := &ReceiveStream{}
+	streamsMap.EXPECT().AcceptUniStream(ctx).Return(str2, nil)
 	ustr, err := tc.conn.AcceptUniStream(ctx)
 	require.NoError(t, err)
-	require.Equal(t, mstr, ustr)
+	require.Equal(t, str2, ustr)
 }
 
 func TestConnectionServerInvalidFrames(t *testing.T) {
diff --git a/http3/client_test.go b/http3/client_test.go
index bc74576a..9dfe27f5 100644
--- a/http3/client_test.go
+++ b/http3/client_test.go
@@ -537,7 +537,7 @@ func testClientStreamHijacking(t *testing.T, bidirectional, doHijack bool, strea
 			return true, nil
 		}
 	case false:
-		tr.UniStreamHijacker = func(st StreamType, id quic.ConnectionTracingID, rs quic.ReceiveStream, e error) (hijacked bool) {
+		tr.UniStreamHijacker = func(st StreamType, id quic.ConnectionTracingID, rs *quic.ReceiveStream, e error) (hijacked bool) {
 			hijackChan <- hijackCall{st: st, connTracingID: id, e: e}
 			return doHijack
 		}
diff --git a/http3/http3_helper_test.go b/http3/http3_helper_test.go
index b5f24b7e..998c36c6 100644
--- a/http3/http3_helper_test.go
+++ b/http3/http3_helper_test.go
@@ -133,7 +133,12 @@ func newConnPairWithDatagrams(t *testing.T) (client, server quic.EarlyConnection
 	return cl, conn
 }
 
-func expectStreamReadReset(t *testing.T, str quic.ReceiveStream, errCode quic.StreamErrorCode) {
+type quicReceiveStream interface {
+	io.Reader
+	SetReadDeadline(time.Time) error
+}
+
+func expectStreamReadReset(t *testing.T, str quicReceiveStream, errCode quic.StreamErrorCode) {
 	t.Helper()
 
 	str.SetReadDeadline(time.Now().Add(time.Second))
diff --git a/http3/server_test.go b/http3/server_test.go
index d963aa62..3c81253f 100644
--- a/http3/server_test.go
+++ b/http3/server_test.go
@@ -495,7 +495,7 @@ func testServerHijackBidirectionalStream(t *testing.T, bidirectional bool, doHij
 			hijackChan <- hijackCall{ft: ft, connTracingID: connTracingID, e: e}
 			return doHijack, hijackErr
 		},
-		UniStreamHijacker: func(st StreamType, connTracingID quic.ConnectionTracingID, _ quic.ReceiveStream, err error) bool {
+		UniStreamHijacker: func(st StreamType, connTracingID quic.ConnectionTracingID, _ *quic.ReceiveStream, err error) bool {
 			defer close(testDone)
 			hijackChan <- hijackCall{st: st, connTracingID: connTracingID, e: err}
 			return doHijack
diff --git a/integrationtests/self/cancelation_test.go b/integrationtests/self/cancelation_test.go
index 4839d7c1..7f0eedeb 100644
--- a/integrationtests/self/cancelation_test.go
+++ b/integrationtests/self/cancelation_test.go
@@ -21,7 +21,7 @@ import (
 
 func TestStreamReadCancellation(t *testing.T) {
 	t.Run("immediate", func(t *testing.T) {
-		testStreamCancellation(t, func(str quic.ReceiveStream) error {
+		testStreamCancellation(t, func(str *quic.ReceiveStream) error {
 			str.CancelRead(quic.StreamErrorCode(str.StreamID()))
 			_, err := str.Read([]byte{0})
 			return err
@@ -29,7 +29,7 @@ func TestStreamReadCancellation(t *testing.T) {
 	})
 
 	t.Run("after reading some data", func(t *testing.T) {
-		testStreamCancellation(t, func(str quic.ReceiveStream) error {
+		testStreamCancellation(t, func(str *quic.ReceiveStream) error {
 			length := rand.IntN(len(PRData) - 1)
 			if _, err := io.ReadAll(io.LimitReader(str, int64(length))); err != nil {
 				return fmt.Errorf("reading stream data failed: %w", err)
@@ -43,7 +43,7 @@ func TestStreamReadCancellation(t *testing.T) {
 	// This test is especially valuable when run with race detector,
 	// see https://github.com/quic-go/quic-go/issues/3239.
 	t.Run("concurrent", func(t *testing.T) {
-		testStreamCancellation(t, func(str quic.ReceiveStream) error {
+		testStreamCancellation(t, func(str *quic.ReceiveStream) error {
 			errChan := make(chan error, 1)
 			go func() {
 				for {
@@ -141,7 +141,7 @@ func TestStreamWriteCancellation(t *testing.T) {
 func TestStreamReadWriteCancellation(t *testing.T) {
 	t.Run("immediate", func(t *testing.T) {
 		testStreamCancellation(t,
-			func(str quic.ReceiveStream) error {
+			func(str *quic.ReceiveStream) error {
 				str.CancelRead(quic.StreamErrorCode(str.StreamID()))
 				_, err := str.Read([]byte{0})
 				return err
@@ -156,7 +156,7 @@ func TestStreamReadWriteCancellation(t *testing.T) {
 
 	t.Run("after writing some data", func(t *testing.T) {
 		testStreamCancellation(t,
-			func(str quic.ReceiveStream) error {
+			func(str *quic.ReceiveStream) error {
 				length := rand.IntN(len(PRData) - 1)
 				if _, err := io.ReadAll(io.LimitReader(str, int64(length))); err != nil {
 					return fmt.Errorf("reading stream data failed: %w", err)
@@ -182,7 +182,7 @@ func TestStreamReadWriteCancellation(t *testing.T) {
 // If writeFunc is set, the write side is canceled for 50% of the streams.
 func testStreamCancellation(
 	t *testing.T,
-	readFunc func(str quic.ReceiveStream) error,
+	readFunc func(str *quic.ReceiveStream) error,
 	writeFunc func(str *quic.SendStream) error,
 ) {
 	const numStreams = 80
@@ -253,7 +253,7 @@ func testStreamCancellation(
 	for _, doCancel := range actions {
 		str, err := conn.AcceptUniStream(ctx)
 		require.NoError(t, err)
-		go func(str quic.ReceiveStream) {
+		go func(str *quic.ReceiveStream) {
 			if readFunc != nil && doCancel {
 				if err := readFunc(str); err != nil {
 					clientErrChan <- &cancellationErr{StreamID: str.StreamID(), Err: err}
@@ -471,7 +471,7 @@ func TestCancelOpenStreamSync(t *testing.T) {
 		<-msg
 		str, err := conn.AcceptUniStream(context.Background())
 		require.NoError(t, err)
-		go func(str quic.ReceiveStream) {
+		go func(str *quic.ReceiveStream) {
 			data, err := io.ReadAll(str)
 			if err != nil {
 				clientErrChan <- err
diff --git a/integrationtests/self/packetization_test.go b/integrationtests/self/packetization_test.go
index e8150ea9..a92bc180 100644
--- a/integrationtests/self/packetization_test.go
+++ b/integrationtests/self/packetization_test.go
@@ -198,12 +198,12 @@ func testConnAndStreamDataBlocked(t *testing.T, limitStream, limitConn bool) {
 	// Stream data is consumed (almost) immediately, so flow-control window auto-tuning kicks in.
 	// The window size is doubled for every batch.
 	var windowSizes []protocol.ByteCount
-	for i := 0; i < numBatches; i++ {
+	for i := range numBatches {
 		windowSizes = append(windowSizes, window<<i)
 	}
 
-	var serverStr quic.ReceiveStream
-	for i := 0; i < numBatches; i++ {
+	var serverStr *quic.ReceiveStream
+	for i := range numBatches {
 		str.SetWriteDeadline(time.Now().Add(rtt))
 		n, err := str.Write(make([]byte, 10000))
 		require.Error(t, err)
diff --git a/internal/ackhandler/ackhandler_suite_test.go b/internal/ackhandler/ackhandler_suite_test.go
deleted file mode 100644
index a0cf3ee1..00000000
--- a/internal/ackhandler/ackhandler_suite_test.go
+++ /dev/null
@@ -1,24 +0,0 @@
-package ackhandler
-
-import (
-	"testing"
-
-	. "github.com/onsi/ginkgo/v2"
-	. "github.com/onsi/gomega"
-	"go.uber.org/mock/gomock"
-)
-
-func TestCrypto(t *testing.T) {
-	RegisterFailHandler(Fail)
-	RunSpecs(t, "AckHandler Suite")
-}
-
-var mockCtrl *gomock.Controller
-
-var _ = BeforeEach(func() {
-	mockCtrl = gomock.NewController(GinkgoT())
-})
-
-var _ = AfterEach(func() {
-	mockCtrl.Finish()
-})
diff --git a/internal/ackhandler/ecn_test.go b/internal/ackhandler/ecn_test.go
index ab648d81..c9a04ae1 100644
--- a/internal/ackhandler/ecn_test.go
+++ b/internal/ackhandler/ecn_test.go
@@ -1,272 +1,279 @@
 package ackhandler
 
 import (
+	"testing"
+
 	mocklogging "github.com/quic-go/quic-go/internal/mocks/logging"
 	"github.com/quic-go/quic-go/internal/protocol"
 	"github.com/quic-go/quic-go/internal/utils"
 	"github.com/quic-go/quic-go/logging"
 
-	. "github.com/onsi/ginkgo/v2"
-	. "github.com/onsi/gomega"
+	"github.com/stretchr/testify/require"
+	"go.uber.org/mock/gomock"
 )
 
-var _ = Describe("ECN tracker", func() {
-	var ecnTracker *ecnTracker
-	var tracer *mocklogging.MockConnectionTracer
+func getAckedPackets(pns ...protocol.PacketNumber) []*packet {
+	var packets []*packet
+	for _, p := range pns {
+		packets = append(packets, &packet{PacketNumber: p})
+	}
+	return packets
+}
 
-	getAckedPackets := func(pns ...protocol.PacketNumber) []*packet {
-		var packets []*packet
-		for _, p := range pns {
-			packets = append(packets, &packet{PacketNumber: p})
-		}
-		return packets
+// sendECNTestingPackets sends 10 ECT(0) packets, and then one more packet
+// Packet numbers: 0 through 9.
+func sendECNTestingPackets(t *testing.T, ecnTracker *ecnTracker, tracer *mocklogging.MockConnectionTracer) {
+	t.Helper()
+
+	tracer.EXPECT().ECNStateUpdated(logging.ECNStateTesting, logging.ECNTriggerNoTrigger)
+	for i := range protocol.PacketNumber(9) {
+		require.Equal(t, protocol.ECT0, ecnTracker.Mode())
+		// do this twice to make sure only sent packets are counted
+		require.Equal(t, protocol.ECT0, ecnTracker.Mode())
+		ecnTracker.SentPacket(i, protocol.ECT0)
 	}
+	require.Equal(t, protocol.ECT0, ecnTracker.Mode())
+	tracer.EXPECT().ECNStateUpdated(logging.ECNStateUnknown, logging.ECNTriggerNoTrigger)
+	ecnTracker.SentPacket(9, protocol.ECT0)
+	// in unknown state, packets shouldn't be ECN-marked
+	require.Equal(t, protocol.ECNNon, ecnTracker.Mode())
+}
 
-	BeforeEach(func() {
-		var tr *logging.ConnectionTracer
-		tr, tracer = mocklogging.NewMockConnectionTracer(mockCtrl)
-		ecnTracker = newECNTracker(utils.DefaultLogger, tr)
-	})
+// ECN validation fails if *all* ECN testing packets are lost.
+func TestECNTestingPacketsLoss(t *testing.T) {
+	mockCtrl := gomock.NewController(t)
+	tr, tracer := mocklogging.NewMockConnectionTracer(mockCtrl)
+	ecnTracker := newECNTracker(utils.DefaultLogger, tr)
 
-	It("sends exactly 10 testing packets", func() {
-		tracer.EXPECT().ECNStateUpdated(logging.ECNStateTesting, logging.ECNTriggerNoTrigger)
-		for i := 0; i < 9; i++ {
-			Expect(ecnTracker.Mode()).To(Equal(protocol.ECT0))
-			// Do this twice to make sure only sent packets are counted
-			Expect(ecnTracker.Mode()).To(Equal(protocol.ECT0))
-			ecnTracker.SentPacket(protocol.PacketNumber(10+i), protocol.ECT0)
-		}
-		Expect(ecnTracker.Mode()).To(Equal(protocol.ECT0))
-		tracer.EXPECT().ECNStateUpdated(logging.ECNStateUnknown, logging.ECNTriggerNoTrigger)
-		ecnTracker.SentPacket(20, protocol.ECT0)
-		// In unknown state, packets shouldn't be ECN-marked.
-		Expect(ecnTracker.Mode()).To(Equal(protocol.ECNNon))
-	})
+	sendECNTestingPackets(t, ecnTracker, tracer)
 
-	sendAllTestingPackets := func() {
-		tracer.EXPECT().ECNStateUpdated(logging.ECNStateTesting, logging.ECNTriggerNoTrigger)
-		tracer.EXPECT().ECNStateUpdated(logging.ECNStateUnknown, logging.ECNTriggerNoTrigger)
-		for i := 0; i < 10; i++ {
-			Expect(ecnTracker.Mode()).To(Equal(protocol.ECT0))
-			ecnTracker.SentPacket(protocol.PacketNumber(i), protocol.ECT0)
-		}
+	// send non-testing packets
+	for i := range protocol.PacketNumber(10) {
+		require.Equal(t, protocol.ECNNon, ecnTracker.Mode())
+		ecnTracker.SentPacket(10+i, protocol.ECNNon)
 	}
 
-	It("fails ECN validation if all ECN testing packets are lost", func() {
-		sendAllTestingPackets()
-		for i := 10; i < 20; i++ {
-			Expect(ecnTracker.Mode()).To(Equal(protocol.ECNNon))
-			ecnTracker.SentPacket(protocol.PacketNumber(i), protocol.ECNNon)
-		}
-		for i := 0; i < 9; i++ {
-			ecnTracker.LostPacket(protocol.PacketNumber(i))
+	// lose all but one packet
+	for pn := range protocol.PacketNumber(10) {
+		if pn == 4 {
+			continue
 		}
-		// We don't care about the loss of non-testing packets
-		ecnTracker.LostPacket(15)
-		// Now lose the last testing packet.
-		tracer.EXPECT().ECNStateUpdated(logging.ECNStateFailed, logging.ECNFailedLostAllTestingPackets)
-		ecnTracker.LostPacket(9)
-		Expect(ecnTracker.Mode()).To(Equal(protocol.ECNNon))
-		// We still don't care about more non-testing packets being lost
-		ecnTracker.LostPacket(16)
-	})
+		ecnTracker.LostPacket(pn)
+	}
+	// loss of non-testing packets doesn't matter
+	ecnTracker.LostPacket(13)
+	ecnTracker.LostPacket(14)
 
-	It("only detects ECN mangling after sending all testing packets", func() {
-		tracer.EXPECT().ECNStateUpdated(logging.ECNStateTesting, logging.ECNTriggerNoTrigger)
-		for i := 0; i < 9; i++ {
-			Expect(ecnTracker.Mode()).To(Equal(protocol.ECT0))
-			ecnTracker.SentPacket(protocol.PacketNumber(i), protocol.ECT0)
-			ecnTracker.LostPacket(protocol.PacketNumber(i))
-		}
-		// Send the last testing packet, and receive a
-		tracer.EXPECT().ECNStateUpdated(logging.ECNStateUnknown, logging.ECNTriggerNoTrigger)
-		Expect(ecnTracker.Mode()).To(Equal(protocol.ECT0))
-		ecnTracker.SentPacket(9, protocol.ECT0)
-		// Now lose the last testing packet.
-		tracer.EXPECT().ECNStateUpdated(logging.ECNStateFailed, logging.ECNFailedLostAllTestingPackets)
-		ecnTracker.LostPacket(9)
-	})
+	// now lose the last testing packet
+	tracer.EXPECT().ECNStateUpdated(logging.ECNStateFailed, logging.ECNFailedLostAllTestingPackets)
+	ecnTracker.LostPacket(4)
+}
 
-	It("passes ECN validation when a testing packet is acknowledged, while still in testing state", func() {
-		tracer.EXPECT().ECNStateUpdated(logging.ECNStateTesting, logging.ECNTriggerNoTrigger)
-		for i := 0; i < 5; i++ {
-			Expect(ecnTracker.Mode()).To(Equal(protocol.ECT0))
-			ecnTracker.SentPacket(protocol.PacketNumber(i), protocol.ECT0)
-		}
-		tracer.EXPECT().ECNStateUpdated(logging.ECNStateCapable, logging.ECNTriggerNoTrigger)
-		Expect(ecnTracker.HandleNewlyAcked(getAckedPackets(3), 1, 0, 0)).To(BeFalse())
-		// make sure we continue sending ECT(0) packets
-		for i := 5; i < 100; i++ {
-			Expect(ecnTracker.Mode()).To(Equal(protocol.ECT0))
-			ecnTracker.SentPacket(protocol.PacketNumber(i), protocol.ECT0)
+// ECN support is validated once an acknowledgment for any testing packet is received.
+// This applies even if that happens before all testing packets have been sent out.
+func TestECNValidationInTestingState(t *testing.T) {
+	mockCtrl := gomock.NewController(t)
+	tr, tracer := mocklogging.NewMockConnectionTracer(mockCtrl)
+	ecnTracker := newECNTracker(utils.DefaultLogger, tr)
+
+	tracer.EXPECT().ECNStateUpdated(logging.ECNStateTesting, logging.ECNTriggerNoTrigger)
+	for i := range 5 {
+		require.Equal(t, protocol.ECT0, ecnTracker.Mode())
+		ecnTracker.SentPacket(protocol.PacketNumber(i), protocol.ECT0)
+	}
+	tracer.EXPECT().ECNStateUpdated(logging.ECNStateCapable, logging.ECNTriggerNoTrigger)
+	require.False(t, ecnTracker.HandleNewlyAcked(getAckedPackets(3), 1, 0, 0))
+	// make sure we continue sending ECT(0) packets
+	for i := 5; i < 100; i++ {
+		require.Equal(t, protocol.ECT0, ecnTracker.Mode())
+		ecnTracker.SentPacket(protocol.PacketNumber(i), protocol.ECT0)
+	}
+}
+
+// ENC is also validated after all testing packets have been sent out,
+// once an acknowledgment for any testing packet is received.
+func TestECNValidationInUnknownState(t *testing.T) {
+	mockCtrl := gomock.NewController(t)
+	tr, tracer := mocklogging.NewMockConnectionTracer(mockCtrl)
+	ecnTracker := newECNTracker(utils.DefaultLogger, tr)
+
+	sendECNTestingPackets(t, ecnTracker, tracer)
+
+	for i := range protocol.PacketNumber(10) {
+		require.Equal(t, protocol.ECNNon, ecnTracker.Mode())
+		pn := 10 + i
+		ecnTracker.SentPacket(pn, protocol.ECNNon)
+		// lose some packets to make sure this doesn't influence the outcome.
+		if i%2 == 0 {
+			ecnTracker.LostPacket(pn)
 		}
+	}
+	tracer.EXPECT().ECNStateUpdated(logging.ECNStateCapable, logging.ECNTriggerNoTrigger)
+	require.False(t, ecnTracker.HandleNewlyAcked(getAckedPackets(7), 1, 0, 0))
+}
+
+func TestECNValidationFailures(t *testing.T) {
+	t.Run("ECN bleaching", func(t *testing.T) {
+		// this ACK doesn't contain any ECN counts
+		testECNValidationFailure(t, getAckedPackets(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12), 0, 0, 0, logging.ECNFailedNoECNCounts)
 	})
 
-	It("passes ECN validation when a testing packet is acknowledged, while in unknown state", func() {
-		sendAllTestingPackets()
-		for i := 10; i < 20; i++ {
-			Expect(ecnTracker.Mode()).To(Equal(protocol.ECNNon))
-			ecnTracker.SentPacket(protocol.PacketNumber(i), protocol.ECNNon)
-		}
-		// Lose some packets to make sure this doesn't influence the outcome.
-		for i := 0; i < 5; i++ {
-			ecnTracker.LostPacket(protocol.PacketNumber(i))
-		}
-		tracer.EXPECT().ECNStateUpdated(logging.ECNStateCapable, logging.ECNTriggerNoTrigger)
-		Expect(ecnTracker.HandleNewlyAcked([]*packet{{PacketNumber: 7}}, 1, 0, 0)).To(BeFalse())
+	t.Run("wrong ECN code point", func(t *testing.T) {
+		// we sent ECT(0), but this ACK acknowledges ECT(1)
+		testECNValidationFailure(t, getAckedPackets(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12), 0, 1, 0, logging.ECNFailedMoreECNCountsThanSent)
 	})
 
-	It("fails ECN validation when the ACK contains more ECN counts than we sent packets", func() {
-		sendAllTestingPackets()
-		for i := 10; i < 20; i++ {
-			Expect(ecnTracker.Mode()).To(Equal(protocol.ECNNon))
-			ecnTracker.SentPacket(protocol.PacketNumber(i), protocol.ECNNon)
-		}
+	t.Run("more ECN counts than sent packets", func(t *testing.T) {
 		// only 10 ECT(0) packets were sent, but the ACK claims to have received 12 of them
-		tracer.EXPECT().ECNStateUpdated(logging.ECNStateFailed, logging.ECNFailedMoreECNCountsThanSent)
-		Expect(ecnTracker.HandleNewlyAcked(getAckedPackets(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12), 12, 0, 0)).To(BeFalse())
+		testECNValidationFailure(t, getAckedPackets(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12), 12, 0, 0, logging.ECNFailedMoreECNCountsThanSent)
 	})
+}
 
-	It("fails ECN validation when the ACK contains ECN counts for the wrong code point", func() {
-		sendAllTestingPackets()
-		for i := 10; i < 20; i++ {
-			Expect(ecnTracker.Mode()).To(Equal(protocol.ECNNon))
-			ecnTracker.SentPacket(protocol.PacketNumber(i), protocol.ECNNon)
-		}
-		// We sent ECT(0), but this ACK acknowledges ECT(1).
-		tracer.EXPECT().ECNStateUpdated(logging.ECNStateFailed, logging.ECNFailedMoreECNCountsThanSent)
-		Expect(ecnTracker.HandleNewlyAcked(getAckedPackets(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12), 0, 1, 0)).To(BeFalse())
-	})
+func testECNValidationFailure(t *testing.T, ackedPackets []*packet, ect0, ect1, ecnce int64, expectedTrigger logging.ECNStateTrigger) {
+	mockCtrl := gomock.NewController(t)
+	tr, tracer := mocklogging.NewMockConnectionTracer(mockCtrl)
+	ecnTracker := newECNTracker(utils.DefaultLogger, tr)
 
-	It("fails ECN validation when the ACK doesn't contain ECN counts", func() {
-		sendAllTestingPackets()
-		for i := 10; i < 20; i++ {
-			Expect(ecnTracker.Mode()).To(Equal(protocol.ECNNon))
-			ecnTracker.SentPacket(protocol.PacketNumber(i), protocol.ECNNon)
-		}
-		// First only acknowledge packets sent without ECN marks.
-		Expect(ecnTracker.HandleNewlyAcked(getAckedPackets(12, 13, 14), 0, 0, 0)).To(BeFalse())
-		// Now acknowledge some packets sent with ECN marks.
-		tracer.EXPECT().ECNStateUpdated(logging.ECNStateFailed, logging.ECNFailedNoECNCounts)
-		Expect(ecnTracker.HandleNewlyAcked(getAckedPackets(1, 2, 3, 15), 0, 0, 0)).To(BeFalse())
-	})
+	sendECNTestingPackets(t, ecnTracker, tracer)
+	for i := 10; i < 20; i++ {
+		require.Equal(t, protocol.ECNNon, ecnTracker.Mode())
+		ecnTracker.SentPacket(protocol.PacketNumber(i), protocol.ECNNon)
+	}
 
-	It("fails ECN validation when an ACK decreases ECN counts", func() {
-		sendAllTestingPackets()
-		for i := 10; i < 20; i++ {
-			Expect(ecnTracker.Mode()).To(Equal(protocol.ECNNon))
-			ecnTracker.SentPacket(protocol.PacketNumber(i), protocol.ECNNon)
-		}
-		tracer.EXPECT().ECNStateUpdated(logging.ECNStateCapable, logging.ECNTriggerNoTrigger)
-		Expect(ecnTracker.HandleNewlyAcked(getAckedPackets(1, 2, 3, 12), 3, 0, 0)).To(BeFalse())
-		// Now acknowledge some more packets, but decrease the ECN counts. Obviously, this doesn't make any sense.
-		tracer.EXPECT().ECNStateUpdated(logging.ECNStateFailed, logging.ECNFailedDecreasedECNCounts)
-		Expect(ecnTracker.HandleNewlyAcked(getAckedPackets(4, 5, 6, 13), 2, 0, 0)).To(BeFalse())
-		// make sure that new ACKs are ignored
-		Expect(ecnTracker.HandleNewlyAcked(getAckedPackets(7, 8, 9, 14), 5, 0, 0)).To(BeFalse())
-	})
+	tracer.EXPECT().ECNStateUpdated(logging.ECNStateFailed, expectedTrigger)
+	require.False(t, ecnTracker.HandleNewlyAcked(ackedPackets, ect0, ect1, ecnce))
+}
 
-	// This can happen if ACK are lost / reordered.
-	It("doesn't fail validation if the ACK contains more ECN counts than it acknowledges packets", func() {
-		sendAllTestingPackets()
-		for i := 10; i < 20; i++ {
-			Expect(ecnTracker.Mode()).To(Equal(protocol.ECNNon))
-			ecnTracker.SentPacket(protocol.PacketNumber(i), protocol.ECNNon)
-		}
-		tracer.EXPECT().ECNStateUpdated(logging.ECNStateCapable, logging.ECNTriggerNoTrigger)
-		Expect(ecnTracker.HandleNewlyAcked(getAckedPackets(1, 2, 3, 12), 8, 0, 0)).To(BeFalse())
-	})
+func TestECNValidationNotEnoughECNCounts(t *testing.T) {
+	mockCtrl := gomock.NewController(t)
+	tr, tracer := mocklogging.NewMockConnectionTracer(mockCtrl)
+	ecnTracker := newECNTracker(utils.DefaultLogger, tr)
 
-	It("fails ECN validation when the ACK doesn't contain enough ECN counts", func() {
-		sendAllTestingPackets()
-		for i := 10; i < 20; i++ {
-			Expect(ecnTracker.Mode()).To(Equal(protocol.ECNNon))
-			ecnTracker.SentPacket(protocol.PacketNumber(i), protocol.ECNNon)
-		}
-		// First only acknowledge some packets sent with ECN marks.
-		tracer.EXPECT().ECNStateUpdated(logging.ECNStateCapable, logging.ECNTriggerNoTrigger)
-		Expect(ecnTracker.HandleNewlyAcked(getAckedPackets(1, 2, 3, 12), 2, 0, 1)).To(BeTrue())
-		// Now acknowledge some more packets sent with ECN marks, but don't increase the counters enough.
-		// This ACK acknowledges 3 more ECN-marked packets, but the counters only increase by 2.
-		tracer.EXPECT().ECNStateUpdated(logging.ECNStateFailed, logging.ECNFailedTooFewECNCounts)
-		Expect(ecnTracker.HandleNewlyAcked(getAckedPackets(4, 5, 6, 15), 3, 0, 2)).To(BeFalse())
-	})
+	sendECNTestingPackets(t, ecnTracker, tracer)
+	for i := 10; i < 20; i++ {
+		require.Equal(t, protocol.ECNNon, ecnTracker.Mode())
+		ecnTracker.SentPacket(protocol.PacketNumber(i), protocol.ECNNon)
+	}
+	// First only acknowledge some packets sent with ECN marks.
+	tracer.EXPECT().ECNStateUpdated(logging.ECNStateCapable, logging.ECNTriggerNoTrigger)
+	require.True(t, ecnTracker.HandleNewlyAcked(getAckedPackets(1, 2, 3, 12), 2, 0, 1))
+	// Now acknowledge some more packets sent with ECN marks, but don't increase the counters enough.
+	// This ACK acknowledges 3 more ECN-marked packets, but the counters only increase by 2.
+	tracer.EXPECT().ECNStateUpdated(logging.ECNStateFailed, logging.ECNFailedTooFewECNCounts)
+	require.False(t, ecnTracker.HandleNewlyAcked(getAckedPackets(4, 5, 6, 15), 3, 0, 2))
+}
 
-	It("detects ECN mangling if all testing packets are marked CE", func() {
-		sendAllTestingPackets()
-		for i := 10; i < 20; i++ {
-			Expect(ecnTracker.Mode()).To(Equal(protocol.ECNNon))
-			ecnTracker.SentPacket(protocol.PacketNumber(i), protocol.ECNNon)
-		}
-		// ECN capability not confirmed yet, therefore CE marks are not regarded as congestion events
-		Expect(ecnTracker.HandleNewlyAcked(getAckedPackets(0, 1, 2, 3), 0, 0, 4)).To(BeFalse())
-		Expect(ecnTracker.HandleNewlyAcked(getAckedPackets(4, 5, 6, 10, 11, 12), 0, 0, 7)).To(BeFalse())
-		// With the next ACK, all testing packets will now have been marked CE.
-		tracer.EXPECT().ECNStateUpdated(logging.ECNStateFailed, logging.ECNFailedManglingDetected)
-		Expect(ecnTracker.HandleNewlyAcked(getAckedPackets(7, 8, 9, 13), 0, 0, 10)).To(BeFalse())
-	})
+func TestECNNonsensicalECNCountDecrease(t *testing.T) {
+	mockCtrl := gomock.NewController(t)
+	tr, tracer := mocklogging.NewMockConnectionTracer(mockCtrl)
+	ecnTracker := newECNTracker(utils.DefaultLogger, tr)
 
-	It("only detects ECN mangling after sending all testing packets", func() {
-		tracer.EXPECT().ECNStateUpdated(logging.ECNStateTesting, logging.ECNTriggerNoTrigger)
-		for i := 0; i < 9; i++ {
-			Expect(ecnTracker.Mode()).To(Equal(protocol.ECT0))
-			ecnTracker.SentPacket(protocol.PacketNumber(i), protocol.ECT0)
-			Expect(ecnTracker.HandleNewlyAcked(getAckedPackets(protocol.PacketNumber(i)), 0, 0, int64(i+1))).To(BeFalse())
-		}
-		// Send the last testing packet, and receive a
-		tracer.EXPECT().ECNStateUpdated(logging.ECNStateUnknown, logging.ECNTriggerNoTrigger)
-		Expect(ecnTracker.Mode()).To(Equal(protocol.ECT0))
-		ecnTracker.SentPacket(9, protocol.ECT0)
-		// This ACK now reports the last testing packets as CE as well.
-		tracer.EXPECT().ECNStateUpdated(logging.ECNStateFailed, logging.ECNFailedManglingDetected)
-		Expect(ecnTracker.HandleNewlyAcked(getAckedPackets(9), 0, 0, 10)).To(BeFalse())
-	})
+	sendECNTestingPackets(t, ecnTracker, tracer)
+	for i := 10; i < 20; i++ {
+		require.Equal(t, protocol.ECNNon, ecnTracker.Mode())
+		ecnTracker.SentPacket(protocol.PacketNumber(i), protocol.ECNNon)
+	}
+	tracer.EXPECT().ECNStateUpdated(logging.ECNStateCapable, logging.ECNTriggerNoTrigger)
+	require.False(t, ecnTracker.HandleNewlyAcked(getAckedPackets(1, 2, 3, 12), 3, 0, 0))
+	// Now acknowledge some more packets, but decrease the ECN counts. Obviously, this doesn't make any sense.
+	tracer.EXPECT().ECNStateUpdated(logging.ECNStateFailed, logging.ECNFailedDecreasedECNCounts)
+	require.False(t, ecnTracker.HandleNewlyAcked(getAckedPackets(4, 5, 6, 13), 2, 0, 0))
+	// make sure that new ACKs are ignored
+	require.False(t, ecnTracker.HandleNewlyAcked(getAckedPackets(7, 8, 9, 14), 5, 0, 0))
+}
 
-	It("detects ECN mangling, if some testing packets are marked CE, and then others are lost", func() {
-		sendAllTestingPackets()
-		for i := 10; i < 20; i++ {
-			Expect(ecnTracker.Mode()).To(Equal(protocol.ECNNon))
-			ecnTracker.SentPacket(protocol.PacketNumber(i), protocol.ECNNon)
-		}
-		// ECN capability not confirmed yet, therefore CE marks are not regarded as congestion events
-		Expect(ecnTracker.HandleNewlyAcked(getAckedPackets(0, 1, 2, 3), 0, 0, 4)).To(BeFalse())
-		Expect(ecnTracker.HandleNewlyAcked(getAckedPackets(6, 7, 8, 9), 0, 0, 8)).To(BeFalse())
-		// Lose one of the two unacknowledged packets.
-		ecnTracker.LostPacket(4)
-		// By losing the last unacknowledged testing packets, we should detect the mangling.
-		tracer.EXPECT().ECNStateUpdated(logging.ECNStateFailed, logging.ECNFailedManglingDetected)
-		ecnTracker.LostPacket(5)
+func TestECNACKReordering(t *testing.T) {
+	mockCtrl := gomock.NewController(t)
+	tr, tracer := mocklogging.NewMockConnectionTracer(mockCtrl)
+	ecnTracker := newECNTracker(utils.DefaultLogger, tr)
+
+	sendECNTestingPackets(t, ecnTracker, tracer)
+	for i := 10; i < 20; i++ {
+		require.Equal(t, protocol.ECNNon, ecnTracker.Mode())
+		ecnTracker.SentPacket(protocol.PacketNumber(i), protocol.ECNNon)
+	}
+	tracer.EXPECT().ECNStateUpdated(logging.ECNStateCapable, logging.ECNTriggerNoTrigger)
+	// The ACK contains more ECN counts than it acknowledges packets.
+	// This can happen if ACKs are lost / reordered.
+	require.False(t, ecnTracker.HandleNewlyAcked(getAckedPackets(1, 2, 3, 12), 8, 0, 0))
+}
+
+// Mangling is detected if all testing packets are marked CE.
+func TestECNManglingAllPacketsMarkedCE(t *testing.T) {
+	mockCtrl := gomock.NewController(t)
+	tr, tracer := mocklogging.NewMockConnectionTracer(mockCtrl)
+	ecnTracker := newECNTracker(utils.DefaultLogger, tr)
+
+	sendECNTestingPackets(t, ecnTracker, tracer)
+	for i := 10; i < 20; i++ {
+		require.Equal(t, protocol.ECNNon, ecnTracker.Mode())
+		ecnTracker.SentPacket(protocol.PacketNumber(i), protocol.ECNNon)
+	}
+	// ECN capability not confirmed yet, therefore CE marks are not regarded as congestion events
+	require.False(t, ecnTracker.HandleNewlyAcked(getAckedPackets(0, 1, 2, 3), 0, 0, 4))
+	require.False(t, ecnTracker.HandleNewlyAcked(getAckedPackets(4, 5, 6, 10, 11, 12), 0, 0, 7))
+	// With the next ACK, all testing packets will now have been marked CE.
+	tracer.EXPECT().ECNStateUpdated(logging.ECNStateFailed, logging.ECNFailedManglingDetected)
+	require.False(t, ecnTracker.HandleNewlyAcked(getAckedPackets(7, 8, 9, 13), 0, 0, 10))
+}
+
+// Mangling is also detected if some testing packets are lost, and then others are marked CE.
+func TestECNManglingSomePacketsLostSomeMarkedCE(t *testing.T) {
+	t.Run("packet loss first", func(t *testing.T) {
+		testECNManglingSomePacketsLostSomeMarkedCE(t, true)
 	})
+	t.Run("CE marking first", func(t *testing.T) {
+		testECNManglingSomePacketsLostSomeMarkedCE(t, false)
+	})
+}
 
-	It("detects ECN mangling, if some testing packets are lost, and then others are marked CE", func() {
-		sendAllTestingPackets()
-		for i := 10; i < 20; i++ {
-			Expect(ecnTracker.Mode()).To(Equal(protocol.ECNNon))
-			ecnTracker.SentPacket(protocol.PacketNumber(i), protocol.ECNNon)
-		}
-		// Lose a few packets.
+func testECNManglingSomePacketsLostSomeMarkedCE(t *testing.T, packetLossFirst bool) {
+	mockCtrl := gomock.NewController(t)
+	tr, tracer := mocklogging.NewMockConnectionTracer(mockCtrl)
+	ecnTracker := newECNTracker(utils.DefaultLogger, tr)
+
+	sendECNTestingPackets(t, ecnTracker, tracer)
+	for i := 10; i < 20; i++ {
+		require.Equal(t, protocol.ECNNon, ecnTracker.Mode())
+		ecnTracker.SentPacket(protocol.PacketNumber(i), protocol.ECNNon)
+	}
+	// Lose a few packets.
+	if packetLossFirst {
 		ecnTracker.LostPacket(0)
 		ecnTracker.LostPacket(1)
 		ecnTracker.LostPacket(2)
-		// ECN capability not confirmed yet, therefore CE marks are not regarded as congestion events
-		Expect(ecnTracker.HandleNewlyAcked(getAckedPackets(3, 4, 5, 6, 7, 8), 0, 0, 6)).To(BeFalse())
-		// By CE-marking the last unacknowledged testing packets, we should detect the mangling.
+	}
+	// ECN capability not confirmed yet, therefore CE marks are not regarded as congestion events
+	require.False(t, ecnTracker.HandleNewlyAcked(getAckedPackets(3, 4, 5, 6, 7, 8), 0, 0, 6))
+	// By CE-marking the last unacknowledged testing packets, we should detect the mangling.
+	if packetLossFirst {
 		tracer.EXPECT().ECNStateUpdated(logging.ECNStateFailed, logging.ECNFailedManglingDetected)
-		Expect(ecnTracker.HandleNewlyAcked(getAckedPackets(9), 0, 0, 7)).To(BeFalse())
-	})
+	}
+	require.False(t, ecnTracker.HandleNewlyAcked(getAckedPackets(9), 0, 0, 7))
 
-	It("declares congestion", func() {
-		sendAllTestingPackets()
-		for i := 10; i < 20; i++ {
-			Expect(ecnTracker.Mode()).To(Equal(protocol.ECNNon))
-			ecnTracker.SentPacket(protocol.PacketNumber(i), protocol.ECNNon)
-		}
-		// Receive one CE count.
-		tracer.EXPECT().ECNStateUpdated(logging.ECNStateCapable, logging.ECNTriggerNoTrigger)
-		Expect(ecnTracker.HandleNewlyAcked(getAckedPackets(1, 2, 3, 12), 2, 0, 1)).To(BeTrue())
-		// No increase in CE. No congestion.
-		Expect(ecnTracker.HandleNewlyAcked(getAckedPackets(4, 5, 6, 13), 5, 0, 1)).To(BeFalse())
-		// Increase in CE. More congestion.
-		Expect(ecnTracker.HandleNewlyAcked(getAckedPackets(7, 8, 9, 14), 7, 0, 2)).To(BeTrue())
-	})
-})
+	if !packetLossFirst {
+		tracer.EXPECT().ECNStateUpdated(logging.ECNStateFailed, logging.ECNFailedManglingDetected)
+		ecnTracker.LostPacket(0)
+		ecnTracker.LostPacket(1)
+		ecnTracker.LostPacket(2)
+	}
+}
+
+func TestECNCongestionDetection(t *testing.T) {
+	mockCtrl := gomock.NewController(t)
+	tr, tracer := mocklogging.NewMockConnectionTracer(mockCtrl)
+	ecnTracker := newECNTracker(utils.DefaultLogger, tr)
+
+	sendECNTestingPackets(t, ecnTracker, tracer)
+	for i := 10; i < 20; i++ {
+		require.Equal(t, protocol.ECNNon, ecnTracker.Mode())
+		ecnTracker.SentPacket(protocol.PacketNumber(i), protocol.ECNNon)
+	}
+	// Receive one CE count.
+	tracer.EXPECT().ECNStateUpdated(logging.ECNStateCapable, logging.ECNTriggerNoTrigger)
+	require.True(t, ecnTracker.HandleNewlyAcked(getAckedPackets(1, 2, 3, 12), 2, 0, 1))
+	// No increase in CE. No congestion.
+	require.False(t, ecnTracker.HandleNewlyAcked(getAckedPackets(4, 5, 6, 13), 5, 0, 1))
+	// Increase in CE. More congestion.
+	require.True(t, ecnTracker.HandleNewlyAcked(getAckedPackets(7, 8, 9, 14), 7, 0, 2))
+}
diff --git a/mock_quic_conn_test.go b/mock_quic_conn_test.go
index c24ace7b..8e0d5353 100644
--- a/mock_quic_conn_test.go
+++ b/mock_quic_conn_test.go
@@ -81,10 +81,10 @@ func (c *MockQUICConnAcceptStreamCall) DoAndReturn(f func(context.Context) (*Str
 }
 
 // AcceptUniStream mocks base method.
-func (m *MockQUICConn) AcceptUniStream(arg0 context.Context) (ReceiveStream, error) {
+func (m *MockQUICConn) AcceptUniStream(arg0 context.Context) (*ReceiveStream, error) {
 	m.ctrl.T.Helper()
 	ret := m.ctrl.Call(m, "AcceptUniStream", arg0)
-	ret0, _ := ret[0].(ReceiveStream)
+	ret0, _ := ret[0].(*ReceiveStream)
 	ret1, _ := ret[1].(error)
 	return ret0, ret1
 }
@@ -102,19 +102,19 @@ type MockQUICConnAcceptUniStreamCall struct {
 }
 
 // Return rewrite *gomock.Call.Return
-func (c *MockQUICConnAcceptUniStreamCall) Return(arg0 ReceiveStream, arg1 error) *MockQUICConnAcceptUniStreamCall {
+func (c *MockQUICConnAcceptUniStreamCall) Return(arg0 *ReceiveStream, arg1 error) *MockQUICConnAcceptUniStreamCall {
 	c.Call = c.Call.Return(arg0, arg1)
 	return c
 }
 
 // Do rewrite *gomock.Call.Do
-func (c *MockQUICConnAcceptUniStreamCall) Do(f func(context.Context) (ReceiveStream, error)) *MockQUICConnAcceptUniStreamCall {
+func (c *MockQUICConnAcceptUniStreamCall) Do(f func(context.Context) (*ReceiveStream, error)) *MockQUICConnAcceptUniStreamCall {
 	c.Call = c.Call.Do(f)
 	return c
 }
 
 // DoAndReturn rewrite *gomock.Call.DoAndReturn
-func (c *MockQUICConnAcceptUniStreamCall) DoAndReturn(f func(context.Context) (ReceiveStream, error)) *MockQUICConnAcceptUniStreamCall {
+func (c *MockQUICConnAcceptUniStreamCall) DoAndReturn(f func(context.Context) (*ReceiveStream, error)) *MockQUICConnAcceptUniStreamCall {
 	c.Call = c.Call.DoAndReturn(f)
 	return c
 }
diff --git a/mock_receive_stream_internal_test.go b/mock_receive_stream_internal_test.go
deleted file mode 100644
index 8455aa30..00000000
--- a/mock_receive_stream_internal_test.go
+++ /dev/null
@@ -1,305 +0,0 @@
-// Code generated by MockGen. DO NOT EDIT.
-// Source: github.com/quic-go/quic-go (interfaces: ReceiveStreamI)
-//
-// Generated by this command:
-//
-//	mockgen -typed -build_flags=-tags=gomock -package quic -self_package github.com/quic-go/quic-go -destination mock_receive_stream_internal_test.go github.com/quic-go/quic-go ReceiveStreamI
-//
-
-// Package quic is a generated GoMock package.
-package quic
-
-import (
-	reflect "reflect"
-	time "time"
-
-	wire "github.com/quic-go/quic-go/internal/wire"
-	gomock "go.uber.org/mock/gomock"
-)
-
-// MockReceiveStreamI is a mock of ReceiveStreamI interface.
-type MockReceiveStreamI struct {
-	ctrl     *gomock.Controller
-	recorder *MockReceiveStreamIMockRecorder
-	isgomock struct{}
-}
-
-// MockReceiveStreamIMockRecorder is the mock recorder for MockReceiveStreamI.
-type MockReceiveStreamIMockRecorder struct {
-	mock *MockReceiveStreamI
-}
-
-// NewMockReceiveStreamI creates a new mock instance.
-func NewMockReceiveStreamI(ctrl *gomock.Controller) *MockReceiveStreamI {
-	mock := &MockReceiveStreamI{ctrl: ctrl}
-	mock.recorder = &MockReceiveStreamIMockRecorder{mock}
-	return mock
-}
-
-// EXPECT returns an object that allows the caller to indicate expected use.
-func (m *MockReceiveStreamI) EXPECT() *MockReceiveStreamIMockRecorder {
-	return m.recorder
-}
-
-// CancelRead mocks base method.
-func (m *MockReceiveStreamI) CancelRead(arg0 StreamErrorCode) {
-	m.ctrl.T.Helper()
-	m.ctrl.Call(m, "CancelRead", arg0)
-}
-
-// CancelRead indicates an expected call of CancelRead.
-func (mr *MockReceiveStreamIMockRecorder) CancelRead(arg0 any) *MockReceiveStreamICancelReadCall {
-	mr.mock.ctrl.T.Helper()
-	call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CancelRead", reflect.TypeOf((*MockReceiveStreamI)(nil).CancelRead), arg0)
-	return &MockReceiveStreamICancelReadCall{Call: call}
-}
-
-// MockReceiveStreamICancelReadCall wrap *gomock.Call
-type MockReceiveStreamICancelReadCall struct {
-	*gomock.Call
-}
-
-// Return rewrite *gomock.Call.Return
-func (c *MockReceiveStreamICancelReadCall) Return() *MockReceiveStreamICancelReadCall {
-	c.Call = c.Call.Return()
-	return c
-}
-
-// Do rewrite *gomock.Call.Do
-func (c *MockReceiveStreamICancelReadCall) Do(f func(StreamErrorCode)) *MockReceiveStreamICancelReadCall {
-	c.Call = c.Call.Do(f)
-	return c
-}
-
-// DoAndReturn rewrite *gomock.Call.DoAndReturn
-func (c *MockReceiveStreamICancelReadCall) DoAndReturn(f func(StreamErrorCode)) *MockReceiveStreamICancelReadCall {
-	c.Call = c.Call.DoAndReturn(f)
-	return c
-}
-
-// Read mocks base method.
-func (m *MockReceiveStreamI) Read(p []byte) (int, error) {
-	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "Read", p)
-	ret0, _ := ret[0].(int)
-	ret1, _ := ret[1].(error)
-	return ret0, ret1
-}
-
-// Read indicates an expected call of Read.
-func (mr *MockReceiveStreamIMockRecorder) Read(p any) *MockReceiveStreamIReadCall {
-	mr.mock.ctrl.T.Helper()
-	call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockReceiveStreamI)(nil).Read), p)
-	return &MockReceiveStreamIReadCall{Call: call}
-}
-
-// MockReceiveStreamIReadCall wrap *gomock.Call
-type MockReceiveStreamIReadCall struct {
-	*gomock.Call
-}
-
-// Return rewrite *gomock.Call.Return
-func (c *MockReceiveStreamIReadCall) Return(n int, err error) *MockReceiveStreamIReadCall {
-	c.Call = c.Call.Return(n, err)
-	return c
-}
-
-// Do rewrite *gomock.Call.Do
-func (c *MockReceiveStreamIReadCall) Do(f func([]byte) (int, error)) *MockReceiveStreamIReadCall {
-	c.Call = c.Call.Do(f)
-	return c
-}
-
-// DoAndReturn rewrite *gomock.Call.DoAndReturn
-func (c *MockReceiveStreamIReadCall) DoAndReturn(f func([]byte) (int, error)) *MockReceiveStreamIReadCall {
-	c.Call = c.Call.DoAndReturn(f)
-	return c
-}
-
-// SetReadDeadline mocks base method.
-func (m *MockReceiveStreamI) SetReadDeadline(t time.Time) error {
-	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "SetReadDeadline", t)
-	ret0, _ := ret[0].(error)
-	return ret0
-}
-
-// SetReadDeadline indicates an expected call of SetReadDeadline.
-func (mr *MockReceiveStreamIMockRecorder) SetReadDeadline(t any) *MockReceiveStreamISetReadDeadlineCall {
-	mr.mock.ctrl.T.Helper()
-	call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetReadDeadline", reflect.TypeOf((*MockReceiveStreamI)(nil).SetReadDeadline), t)
-	return &MockReceiveStreamISetReadDeadlineCall{Call: call}
-}
-
-// MockReceiveStreamISetReadDeadlineCall wrap *gomock.Call
-type MockReceiveStreamISetReadDeadlineCall struct {
-	*gomock.Call
-}
-
-// Return rewrite *gomock.Call.Return
-func (c *MockReceiveStreamISetReadDeadlineCall) Return(arg0 error) *MockReceiveStreamISetReadDeadlineCall {
-	c.Call = c.Call.Return(arg0)
-	return c
-}
-
-// Do rewrite *gomock.Call.Do
-func (c *MockReceiveStreamISetReadDeadlineCall) Do(f func(time.Time) error) *MockReceiveStreamISetReadDeadlineCall {
-	c.Call = c.Call.Do(f)
-	return c
-}
-
-// DoAndReturn rewrite *gomock.Call.DoAndReturn
-func (c *MockReceiveStreamISetReadDeadlineCall) DoAndReturn(f func(time.Time) error) *MockReceiveStreamISetReadDeadlineCall {
-	c.Call = c.Call.DoAndReturn(f)
-	return c
-}
-
-// StreamID mocks base method.
-func (m *MockReceiveStreamI) StreamID() StreamID {
-	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "StreamID")
-	ret0, _ := ret[0].(StreamID)
-	return ret0
-}
-
-// StreamID indicates an expected call of StreamID.
-func (mr *MockReceiveStreamIMockRecorder) StreamID() *MockReceiveStreamIStreamIDCall {
-	mr.mock.ctrl.T.Helper()
-	call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StreamID", reflect.TypeOf((*MockReceiveStreamI)(nil).StreamID))
-	return &MockReceiveStreamIStreamIDCall{Call: call}
-}
-
-// MockReceiveStreamIStreamIDCall wrap *gomock.Call
-type MockReceiveStreamIStreamIDCall struct {
-	*gomock.Call
-}
-
-// Return rewrite *gomock.Call.Return
-func (c *MockReceiveStreamIStreamIDCall) Return(arg0 StreamID) *MockReceiveStreamIStreamIDCall {
-	c.Call = c.Call.Return(arg0)
-	return c
-}
-
-// Do rewrite *gomock.Call.Do
-func (c *MockReceiveStreamIStreamIDCall) Do(f func() StreamID) *MockReceiveStreamIStreamIDCall {
-	c.Call = c.Call.Do(f)
-	return c
-}
-
-// DoAndReturn rewrite *gomock.Call.DoAndReturn
-func (c *MockReceiveStreamIStreamIDCall) DoAndReturn(f func() StreamID) *MockReceiveStreamIStreamIDCall {
-	c.Call = c.Call.DoAndReturn(f)
-	return c
-}
-
-// closeForShutdown mocks base method.
-func (m *MockReceiveStreamI) closeForShutdown(arg0 error) {
-	m.ctrl.T.Helper()
-	m.ctrl.Call(m, "closeForShutdown", arg0)
-}
-
-// closeForShutdown indicates an expected call of closeForShutdown.
-func (mr *MockReceiveStreamIMockRecorder) closeForShutdown(arg0 any) *MockReceiveStreamIcloseForShutdownCall {
-	mr.mock.ctrl.T.Helper()
-	call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "closeForShutdown", reflect.TypeOf((*MockReceiveStreamI)(nil).closeForShutdown), arg0)
-	return &MockReceiveStreamIcloseForShutdownCall{Call: call}
-}
-
-// MockReceiveStreamIcloseForShutdownCall wrap *gomock.Call
-type MockReceiveStreamIcloseForShutdownCall struct {
-	*gomock.Call
-}
-
-// Return rewrite *gomock.Call.Return
-func (c *MockReceiveStreamIcloseForShutdownCall) Return() *MockReceiveStreamIcloseForShutdownCall {
-	c.Call = c.Call.Return()
-	return c
-}
-
-// Do rewrite *gomock.Call.Do
-func (c *MockReceiveStreamIcloseForShutdownCall) Do(f func(error)) *MockReceiveStreamIcloseForShutdownCall {
-	c.Call = c.Call.Do(f)
-	return c
-}
-
-// DoAndReturn rewrite *gomock.Call.DoAndReturn
-func (c *MockReceiveStreamIcloseForShutdownCall) DoAndReturn(f func(error)) *MockReceiveStreamIcloseForShutdownCall {
-	c.Call = c.Call.DoAndReturn(f)
-	return c
-}
-
-// handleResetStreamFrame mocks base method.
-func (m *MockReceiveStreamI) handleResetStreamFrame(arg0 *wire.ResetStreamFrame, arg1 time.Time) error {
-	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "handleResetStreamFrame", arg0, arg1)
-	ret0, _ := ret[0].(error)
-	return ret0
-}
-
-// handleResetStreamFrame indicates an expected call of handleResetStreamFrame.
-func (mr *MockReceiveStreamIMockRecorder) handleResetStreamFrame(arg0, arg1 any) *MockReceiveStreamIhandleResetStreamFrameCall {
-	mr.mock.ctrl.T.Helper()
-	call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "handleResetStreamFrame", reflect.TypeOf((*MockReceiveStreamI)(nil).handleResetStreamFrame), arg0, arg1)
-	return &MockReceiveStreamIhandleResetStreamFrameCall{Call: call}
-}
-
-// MockReceiveStreamIhandleResetStreamFrameCall wrap *gomock.Call
-type MockReceiveStreamIhandleResetStreamFrameCall struct {
-	*gomock.Call
-}
-
-// Return rewrite *gomock.Call.Return
-func (c *MockReceiveStreamIhandleResetStreamFrameCall) Return(arg0 error) *MockReceiveStreamIhandleResetStreamFrameCall {
-	c.Call = c.Call.Return(arg0)
-	return c
-}
-
-// Do rewrite *gomock.Call.Do
-func (c *MockReceiveStreamIhandleResetStreamFrameCall) Do(f func(*wire.ResetStreamFrame, time.Time) error) *MockReceiveStreamIhandleResetStreamFrameCall {
-	c.Call = c.Call.Do(f)
-	return c
-}
-
-// DoAndReturn rewrite *gomock.Call.DoAndReturn
-func (c *MockReceiveStreamIhandleResetStreamFrameCall) DoAndReturn(f func(*wire.ResetStreamFrame, time.Time) error) *MockReceiveStreamIhandleResetStreamFrameCall {
-	c.Call = c.Call.DoAndReturn(f)
-	return c
-}
-
-// handleStreamFrame mocks base method.
-func (m *MockReceiveStreamI) handleStreamFrame(arg0 *wire.StreamFrame, arg1 time.Time) error {
-	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "handleStreamFrame", arg0, arg1)
-	ret0, _ := ret[0].(error)
-	return ret0
-}
-
-// handleStreamFrame indicates an expected call of handleStreamFrame.
-func (mr *MockReceiveStreamIMockRecorder) handleStreamFrame(arg0, arg1 any) *MockReceiveStreamIhandleStreamFrameCall {
-	mr.mock.ctrl.T.Helper()
-	call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "handleStreamFrame", reflect.TypeOf((*MockReceiveStreamI)(nil).handleStreamFrame), arg0, arg1)
-	return &MockReceiveStreamIhandleStreamFrameCall{Call: call}
-}
-
-// MockReceiveStreamIhandleStreamFrameCall wrap *gomock.Call
-type MockReceiveStreamIhandleStreamFrameCall struct {
-	*gomock.Call
-}
-
-// Return rewrite *gomock.Call.Return
-func (c *MockReceiveStreamIhandleStreamFrameCall) Return(arg0 error) *MockReceiveStreamIhandleStreamFrameCall {
-	c.Call = c.Call.Return(arg0)
-	return c
-}
-
-// Do rewrite *gomock.Call.Do
-func (c *MockReceiveStreamIhandleStreamFrameCall) Do(f func(*wire.StreamFrame, time.Time) error) *MockReceiveStreamIhandleStreamFrameCall {
-	c.Call = c.Call.Do(f)
-	return c
-}
-
-// DoAndReturn rewrite *gomock.Call.DoAndReturn
-func (c *MockReceiveStreamIhandleStreamFrameCall) DoAndReturn(f func(*wire.StreamFrame, time.Time) error) *MockReceiveStreamIhandleStreamFrameCall {
-	c.Call = c.Call.DoAndReturn(f)
-	return c
-}
diff --git a/mock_stream_manager_test.go b/mock_stream_manager_test.go
index b87955ea..6b7f744e 100644
--- a/mock_stream_manager_test.go
+++ b/mock_stream_manager_test.go
@@ -82,10 +82,10 @@ func (c *MockStreamManagerAcceptStreamCall) DoAndReturn(f func(context.Context)
 }
 
 // AcceptUniStream mocks base method.
-func (m *MockStreamManager) AcceptUniStream(arg0 context.Context) (ReceiveStream, error) {
+func (m *MockStreamManager) AcceptUniStream(arg0 context.Context) (*ReceiveStream, error) {
 	m.ctrl.T.Helper()
 	ret := m.ctrl.Call(m, "AcceptUniStream", arg0)
-	ret0, _ := ret[0].(ReceiveStream)
+	ret0, _ := ret[0].(*ReceiveStream)
 	ret1, _ := ret[1].(error)
 	return ret0, ret1
 }
@@ -103,19 +103,19 @@ type MockStreamManagerAcceptUniStreamCall struct {
 }
 
 // Return rewrite *gomock.Call.Return
-func (c *MockStreamManagerAcceptUniStreamCall) Return(arg0 ReceiveStream, arg1 error) *MockStreamManagerAcceptUniStreamCall {
+func (c *MockStreamManagerAcceptUniStreamCall) Return(arg0 *ReceiveStream, arg1 error) *MockStreamManagerAcceptUniStreamCall {
 	c.Call = c.Call.Return(arg0, arg1)
 	return c
 }
 
 // Do rewrite *gomock.Call.Do
-func (c *MockStreamManagerAcceptUniStreamCall) Do(f func(context.Context) (ReceiveStream, error)) *MockStreamManagerAcceptUniStreamCall {
+func (c *MockStreamManagerAcceptUniStreamCall) Do(f func(context.Context) (*ReceiveStream, error)) *MockStreamManagerAcceptUniStreamCall {
 	c.Call = c.Call.Do(f)
 	return c
 }
 
 // DoAndReturn rewrite *gomock.Call.DoAndReturn
-func (c *MockStreamManagerAcceptUniStreamCall) DoAndReturn(f func(context.Context) (ReceiveStream, error)) *MockStreamManagerAcceptUniStreamCall {
+func (c *MockStreamManagerAcceptUniStreamCall) DoAndReturn(f func(context.Context) (*ReceiveStream, error)) *MockStreamManagerAcceptUniStreamCall {
 	c.Call = c.Call.DoAndReturn(f)
 	return c
 }
@@ -195,10 +195,10 @@ func (c *MockStreamManagerDeleteStreamCall) DoAndReturn(f func(protocol.StreamID
 }
 
 // GetOrOpenReceiveStream mocks base method.
-func (m *MockStreamManager) GetOrOpenReceiveStream(arg0 protocol.StreamID) (receiveStreamI, error) {
+func (m *MockStreamManager) GetOrOpenReceiveStream(arg0 protocol.StreamID) (*ReceiveStream, error) {
 	m.ctrl.T.Helper()
 	ret := m.ctrl.Call(m, "GetOrOpenReceiveStream", arg0)
-	ret0, _ := ret[0].(receiveStreamI)
+	ret0, _ := ret[0].(*ReceiveStream)
 	ret1, _ := ret[1].(error)
 	return ret0, ret1
 }
@@ -216,19 +216,19 @@ type MockStreamManagerGetOrOpenReceiveStreamCall struct {
 }
 
 // Return rewrite *gomock.Call.Return
-func (c *MockStreamManagerGetOrOpenReceiveStreamCall) Return(arg0 receiveStreamI, arg1 error) *MockStreamManagerGetOrOpenReceiveStreamCall {
+func (c *MockStreamManagerGetOrOpenReceiveStreamCall) Return(arg0 *ReceiveStream, arg1 error) *MockStreamManagerGetOrOpenReceiveStreamCall {
 	c.Call = c.Call.Return(arg0, arg1)
 	return c
 }
 
 // Do rewrite *gomock.Call.Do
-func (c *MockStreamManagerGetOrOpenReceiveStreamCall) Do(f func(protocol.StreamID) (receiveStreamI, error)) *MockStreamManagerGetOrOpenReceiveStreamCall {
+func (c *MockStreamManagerGetOrOpenReceiveStreamCall) Do(f func(protocol.StreamID) (*ReceiveStream, error)) *MockStreamManagerGetOrOpenReceiveStreamCall {
 	c.Call = c.Call.Do(f)
 	return c
 }
 
 // DoAndReturn rewrite *gomock.Call.DoAndReturn
-func (c *MockStreamManagerGetOrOpenReceiveStreamCall) DoAndReturn(f func(protocol.StreamID) (receiveStreamI, error)) *MockStreamManagerGetOrOpenReceiveStreamCall {
+func (c *MockStreamManagerGetOrOpenReceiveStreamCall) DoAndReturn(f func(protocol.StreamID) (*ReceiveStream, error)) *MockStreamManagerGetOrOpenReceiveStreamCall {
 	c.Call = c.Call.DoAndReturn(f)
 	return c
 }