From d1f64cc67036f272ea900194674afcf71eb3e816 Mon Sep 17 00:00:00 2001 From: Damien Neil Date: Thu, 23 Oct 2025 15:17:56 -0700 Subject: [PATCH] quic: use testing/synctest Replace bespoke fake time and synchronization with testing/synctest. Change-Id: Ic3fe9635dbad36c890783c38e00708c6cb7a15f8 Reviewed-on: https://go-review.googlesource.com/c/net/+/714482 Reviewed-by: Nicholas Husin Reviewed-by: Nicholas Husin LUCI-TryBot-Result: Go LUCI Auto-Submit: Damien Neil --- quic/bench_test.go | 2 + quic/config_test.go | 10 ++- quic/conn.go | 71 +++-------------- quic/conn_async_test.go | 133 ++++---------------------------- quic/conn_close_test.go | 48 ++++++++++-- quic/conn_flow_test.go | 39 ++++++++++ quic/conn_id_test.go | 63 ++++++++++++++- quic/conn_loss_test.go | 56 ++++++++++---- quic/conn_recv_test.go | 9 +++ quic/conn_send_test.go | 11 ++- quic/conn_streams.go | 2 +- quic/conn_streams_test.go | 37 ++++++++- quic/conn_test.go | 144 +++++++---------------------------- quic/endpoint.go | 10 +-- quic/endpoint_test.go | 54 ++++--------- quic/gate.go | 5 +- quic/gate_test.go | 8 +- quic/idle_test.go | 41 ++++++---- quic/key_update_test.go | 15 ++++ quic/packet_codec_test.go | 2 + quic/path_test.go | 44 ++++++----- quic/ping_test.go | 13 +++- quic/qlog_test.go | 31 ++++++-- quic/queue.go | 4 +- quic/queue_test.go | 12 +-- quic/quic_test.go | 40 ++++++++++ quic/retry_test.go | 39 +++++++++- quic/skip_test.go | 13 +++- quic/stateless_reset_test.go | 22 +++++- quic/stream.go | 4 +- quic/stream_limits.go | 2 +- quic/stream_limits_test.go | 20 +++-- quic/stream_test.go | 109 +++++++++++++++++++------- quic/tls_test.go | 53 ++++++++++--- quic/version_test.go | 15 ++++ 35 files changed, 702 insertions(+), 479 deletions(-) diff --git a/quic/bench_test.go b/quic/bench_test.go index 9d8e5d23..002b40e6 100644 --- a/quic/bench_test.go +++ b/quic/bench_test.go @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +//go:build go1.25 + package quic import ( diff --git a/quic/config_test.go b/quic/config_test.go index 3511cd4a..df878dab 100644 --- a/quic/config_test.go +++ b/quic/config_test.go @@ -2,11 +2,19 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +//go:build go1.25 + package quic -import "testing" +import ( + "testing" + "testing/synctest" +) func TestConfigTransportParameters(t *testing.T) { + synctest.Test(t, testConfigTransportParameters) +} +func testConfigTransportParameters(t *testing.T) { const ( wantInitialMaxData = int64(1) wantInitialMaxStreamData = int64(2) diff --git a/quic/conn.go b/quic/conn.go index 40bdddc2..fd812b8a 100644 --- a/quic/conn.go +++ b/quic/conn.go @@ -69,23 +69,12 @@ type connTestHooks interface { // init is called after a conn is created. init(first bool) - // nextMessage is called to request the next event from msgc. - // Used to give tests control of the connection event loop. - nextMessage(msgc chan any, nextTimeout time.Time) (now time.Time, message any) - // handleTLSEvent is called with each TLS event. handleTLSEvent(tls.QUICEvent) // newConnID is called to generate a new connection ID. // Permits tests to generate consistent connection IDs rather than random ones. newConnID(seq int64) ([]byte, error) - - // waitUntil blocks until the until func returns true or the context is done. - // Used to synchronize asynchronous blocking operations in tests. - waitUntil(ctx context.Context, until func() bool) error - - // timeNow returns the current time. - timeNow() time.Time } // newServerConnIDs is connection IDs associated with a new server connection. @@ -102,7 +91,6 @@ func newConn(now time.Time, side connSide, cids newServerConnIDs, peerHostname s endpoint: e, config: config, peerAddr: unmapAddrPort(peerAddr), - msgc: make(chan any, 1), donec: make(chan struct{}), peerAckDelayExponent: -1, } @@ -299,17 +287,12 @@ func (c *Conn) loop(now time.Time) { // The connection timer sends a message to the connection loop on expiry. // We need to give it an expiry when creating it, so set the initial timeout to // an arbitrary large value. The timer will be reset before this expires (and it - // isn't a problem if it does anyway). Skip creating the timer in tests which - // take control of the connection message loop. - var timer *time.Timer + // isn't a problem if it does anyway). var lastTimeout time.Time - hooks := c.testHooks - if hooks == nil { - timer = time.AfterFunc(1*time.Hour, func() { - c.sendMsg(timerEvent{}) - }) - defer timer.Stop() - } + timer := time.AfterFunc(1*time.Hour, func() { + c.sendMsg(timerEvent{}) + }) + defer timer.Stop() for c.lifetime.state != connStateDone { sendTimeout := c.maybeSend(now) // try sending @@ -326,10 +309,7 @@ func (c *Conn) loop(now time.Time) { } var m any - if hooks != nil { - // Tests only: Wait for the test to tell us to continue. - now, m = hooks.nextMessage(c.msgc, nextTimeout) - } else if !nextTimeout.IsZero() && nextTimeout.Before(now) { + if !nextTimeout.IsZero() && nextTimeout.Before(now) { // A connection timer has expired. now = time.Now() m = timerEvent{} @@ -372,6 +352,9 @@ func (c *Conn) loop(now time.Time) { case func(time.Time, *Conn): // Send a func to msgc to run it on the main Conn goroutine m(now, c) + case func(now, next time.Time, _ *Conn): + // Send a func to msgc to run it on the main Conn goroutine + m(now, nextTimeout, c) default: panic(fmt.Sprintf("quic: unrecognized conn message %T", m)) } @@ -410,31 +393,7 @@ func (c *Conn) runOnLoop(ctx context.Context, f func(now time.Time, c *Conn)) er defer close(donec) f(now, c) } - if c.testHooks != nil { - // In tests, we can't rely on being able to send a message immediately: - // c.msgc might be full, and testConnHooks.nextMessage might be waiting - // for us to block before it processes the next message. - // To avoid a deadlock, we send the message in waitUntil. - // If msgc is empty, the message is buffered. - // If msgc is full, we block and let nextMessage process the queue. - msgc := c.msgc - c.testHooks.waitUntil(ctx, func() bool { - for { - select { - case msgc <- msg: - msgc = nil // send msg only once - case <-donec: - return true - case <-c.donec: - return true - default: - return false - } - } - }) - } else { - c.sendMsg(msg) - } + c.sendMsg(msg) select { case <-donec: case <-c.donec: @@ -444,16 +403,6 @@ func (c *Conn) runOnLoop(ctx context.Context, f func(now time.Time, c *Conn)) er } func (c *Conn) waitOnDone(ctx context.Context, ch <-chan struct{}) error { - if c.testHooks != nil { - return c.testHooks.waitUntil(ctx, func() bool { - select { - case <-ch: - return true - default: - } - return false - }) - } // Check the channel before the context. // We always prefer to return results when available, // even when provided with an already-canceled context. diff --git a/quic/conn_async_test.go b/quic/conn_async_test.go index f261e900..08cc7d33 100644 --- a/quic/conn_async_test.go +++ b/quic/conn_async_test.go @@ -2,44 +2,21 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +//go:build go1.25 + package quic import ( "context" "errors" "fmt" - "path/filepath" - "runtime" - "sync" + "testing/synctest" ) -// asyncTestState permits handling asynchronous operations in a synchronous test. -// -// For example, a test may want to write to a stream and observe that -// STREAM frames are sent with the contents of the write in response -// to MAX_STREAM_DATA frames received from the peer. -// The Stream.Write is an asynchronous operation, but the test is simpler -// if we can start the write, observe the first STREAM frame sent, -// send a MAX_STREAM_DATA frame, observe the next STREAM frame sent, etc. -// -// We do this by instrumenting points where operations can block. -// We start async operations like Write in a goroutine, -// and wait for the operation to either finish or hit a blocking point. -// When the connection event loop is idle, we check a list of -// blocked operations to see if any can be woken. -type asyncTestState struct { - mu sync.Mutex - notify chan struct{} - blocked map[*blockedAsync]struct{} -} - // An asyncOp is an asynchronous operation that results in (T, error). type asyncOp[T any] struct { - v T - err error - - caller string - tc *testConn + v T + err error donec chan struct{} cancelFunc context.CancelFunc } @@ -47,17 +24,18 @@ type asyncOp[T any] struct { // cancel cancels the async operation's context, and waits for // the operation to complete. func (a *asyncOp[T]) cancel() { + synctest.Wait() select { case <-a.donec: return // already done default: } a.cancelFunc() - <-a.tc.asyncTestState.notify + synctest.Wait() select { case <-a.donec: default: - panic(fmt.Errorf("%v: async op failed to finish after being canceled", a.caller)) + panic(fmt.Errorf("async op failed to finish after being canceled")) } } @@ -71,115 +49,30 @@ var errNotDone = errors.New("async op is not done") // control over the progress of operations, an asyncOp can only // become done in reaction to the test taking some action. func (a *asyncOp[T]) result() (v T, err error) { - a.tc.wait() + synctest.Wait() select { case <-a.donec: return a.v, a.err default: - return v, errNotDone + return a.v, errNotDone } } -// A blockedAsync is a blocked async operation. -type blockedAsync struct { - until func() bool // when this returns true, the operation is unblocked - donec chan struct{} // closed when the operation is unblocked -} - -type asyncContextKey struct{} - // runAsync starts an asynchronous operation. // // The function f should call a blocking function such as // Stream.Write or Conn.AcceptStream and return its result. // It must use the provided context. func runAsync[T any](tc *testConn, f func(context.Context) (T, error)) *asyncOp[T] { - as := &tc.asyncTestState - if as.notify == nil { - as.notify = make(chan struct{}) - as.mu.Lock() - as.blocked = make(map[*blockedAsync]struct{}) - as.mu.Unlock() - } - _, file, line, _ := runtime.Caller(1) - ctx := context.WithValue(context.Background(), asyncContextKey{}, true) - ctx, cancel := context.WithCancel(ctx) + ctx, cancel := context.WithCancel(tc.t.Context()) a := &asyncOp[T]{ - tc: tc, - caller: fmt.Sprintf("%v:%v", filepath.Base(file), line), donec: make(chan struct{}), cancelFunc: cancel, } go func() { + defer close(a.donec) a.v, a.err = f(ctx) - close(a.donec) - as.notify <- struct{}{} }() - tc.t.Cleanup(func() { - if _, err := a.result(); err == errNotDone { - tc.t.Errorf("%v: async operation is still executing at end of test", a.caller) - a.cancel() - } - }) - // Wait for the operation to either finish or block. - <-as.notify - tc.wait() + synctest.Wait() return a } - -// waitUntil waits for a blocked async operation to complete. -// The operation is complete when the until func returns true. -func (as *asyncTestState) waitUntil(ctx context.Context, until func() bool) error { - if until() { - return nil - } - if err := ctx.Err(); err != nil { - // Context has already expired. - return err - } - if ctx.Value(asyncContextKey{}) == nil { - // Context is not one that we've created, and hasn't expired. - // This probably indicates that we've tried to perform a - // blocking operation without using the async test harness here, - // which may have unpredictable results. - panic("blocking async point with unexpected Context") - } - b := &blockedAsync{ - until: until, - donec: make(chan struct{}), - } - // Record this as a pending blocking operation. - as.mu.Lock() - as.blocked[b] = struct{}{} - as.mu.Unlock() - // Notify the creator of the operation that we're blocked, - // and wait to be woken up. - as.notify <- struct{}{} - select { - case <-b.donec: - case <-ctx.Done(): - return ctx.Err() - } - return nil -} - -// wakeAsync tries to wake up a blocked async operation. -// It returns true if one was woken, false otherwise. -func (as *asyncTestState) wakeAsync() bool { - as.mu.Lock() - var woken *blockedAsync - for w := range as.blocked { - if w.until() { - woken = w - delete(as.blocked, w) - break - } - } - as.mu.Unlock() - if woken == nil { - return false - } - close(woken.donec) - <-as.notify // must not hold as.mu while blocked here - return true -} diff --git a/quic/conn_close_test.go b/quic/conn_close_test.go index 0b37b3ec..472a8f2d 100644 --- a/quic/conn_close_test.go +++ b/quic/conn_close_test.go @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +//go:build go1.25 + package quic import ( @@ -9,10 +11,14 @@ import ( "crypto/tls" "errors" "testing" + "testing/synctest" "time" ) func TestConnCloseResponseBackoff(t *testing.T) { + synctest.Test(t, testConnCloseResponseBackoff) +} +func testConnCloseResponseBackoff(t *testing.T) { tc := newTestConn(t, clientSide, func(c *Config) { clear(c.StatelessResetKey[:]) }) @@ -34,18 +40,18 @@ func TestConnCloseResponseBackoff(t *testing.T) { tc.writeFrames(packetType1RTT, debugFramePing{}) tc.wantIdle("packets received immediately after CONN_CLOSE receive no response") - tc.advance(1100 * time.Microsecond) + time.Sleep(1100 * time.Microsecond) tc.writeFrames(packetType1RTT, debugFramePing{}) tc.wantFrame("receiving packet 1.1ms after CONN_CLOSE generates another CONN_CLOSE", packetType1RTT, debugFrameConnectionCloseTransport{ code: errNo, }) - tc.advance(1100 * time.Microsecond) + time.Sleep(1100 * time.Microsecond) tc.writeFrames(packetType1RTT, debugFramePing{}) tc.wantIdle("no response to packet, because CONN_CLOSE backoff is now 2ms") - tc.advance(1000 * time.Microsecond) + time.Sleep(1000 * time.Microsecond) tc.writeFrames(packetType1RTT, debugFramePing{}) tc.wantFrame("2ms since last CONN_CLOSE, receiving a packet generates another CONN_CLOSE", packetType1RTT, debugFrameConnectionCloseTransport{ @@ -55,7 +61,7 @@ func TestConnCloseResponseBackoff(t *testing.T) { t.Errorf("conn.Wait() = %v, want still waiting", err) } - tc.advance(100000 * time.Microsecond) + time.Sleep(100000 * time.Microsecond) tc.writeFrames(packetType1RTT, debugFramePing{}) tc.wantIdle("drain timer expired, no more responses") @@ -68,6 +74,9 @@ func TestConnCloseResponseBackoff(t *testing.T) { } func TestConnCloseWithPeerResponse(t *testing.T) { + synctest.Test(t, testConnCloseWithPeerResponse) +} +func testConnCloseWithPeerResponse(t *testing.T) { qr := &qlogRecord{} tc := newTestConn(t, clientSide, qr.config) tc.handshake() @@ -99,7 +108,7 @@ func TestConnCloseWithPeerResponse(t *testing.T) { t.Errorf("non-blocking conn.Wait() = %v, want %v", err, wantErr) } - tc.advance(1 * time.Second) // long enough to exit the draining state + time.Sleep(1 * time.Second) // long enough to exit the draining state qr.wantEvents(t, jsonEvent{ "name": "connectivity:connection_closed", "data": map[string]any{ @@ -109,6 +118,9 @@ func TestConnCloseWithPeerResponse(t *testing.T) { } func TestConnClosePeerCloses(t *testing.T) { + synctest.Test(t, testConnClosePeerCloses) +} +func testConnClosePeerCloses(t *testing.T) { qr := &qlogRecord{} tc := newTestConn(t, clientSide, qr.config) tc.handshake() @@ -137,7 +149,7 @@ func TestConnClosePeerCloses(t *testing.T) { reason: "because", }) - tc.advance(1 * time.Second) // long enough to exit the draining state + time.Sleep(1 * time.Second) // long enough to exit the draining state qr.wantEvents(t, jsonEvent{ "name": "connectivity:connection_closed", "data": map[string]any{ @@ -147,6 +159,9 @@ func TestConnClosePeerCloses(t *testing.T) { } func TestConnCloseReceiveInInitial(t *testing.T) { + synctest.Test(t, testConnCloseReceiveInInitial) +} +func testConnCloseReceiveInInitial(t *testing.T) { tc := newTestConn(t, clientSide) tc.wantFrame("client sends Initial CRYPTO frame", packetTypeInitial, debugFrameCrypto{ @@ -171,6 +186,9 @@ func TestConnCloseReceiveInInitial(t *testing.T) { } func TestConnCloseReceiveInHandshake(t *testing.T) { + synctest.Test(t, testConnCloseReceiveInHandshake) +} +func testConnCloseReceiveInHandshake(t *testing.T) { tc := newTestConn(t, clientSide) tc.ignoreFrame(frameTypeAck) tc.wantFrame("client sends Initial CRYPTO frame", @@ -204,6 +222,9 @@ func TestConnCloseReceiveInHandshake(t *testing.T) { } func TestConnCloseClosedByEndpoint(t *testing.T) { + synctest.Test(t, testConnCloseClosedByEndpoint) +} +func testConnCloseClosedByEndpoint(t *testing.T) { ctx := canceledContext() tc := newTestConn(t, clientSide) tc.handshake() @@ -231,6 +252,9 @@ func testConnCloseUnblocks(t *testing.T, f func(context.Context, *testConn) erro } func TestConnCloseUnblocksAcceptStream(t *testing.T) { + synctest.Test(t, testConnCloseUnblocksAcceptStream) +} +func testConnCloseUnblocksAcceptStream(t *testing.T) { testConnCloseUnblocks(t, func(ctx context.Context, tc *testConn) error { _, err := tc.conn.AcceptStream(ctx) return err @@ -238,6 +262,9 @@ func TestConnCloseUnblocksAcceptStream(t *testing.T) { } func TestConnCloseUnblocksNewStream(t *testing.T) { + synctest.Test(t, testConnCloseUnblocksNewStream) +} +func testConnCloseUnblocksNewStream(t *testing.T) { testConnCloseUnblocks(t, func(ctx context.Context, tc *testConn) error { _, err := tc.conn.NewStream(ctx) return err @@ -245,6 +272,9 @@ func TestConnCloseUnblocksNewStream(t *testing.T) { } func TestConnCloseUnblocksStreamRead(t *testing.T) { + synctest.Test(t, testConnCloseUnblocksStreamRead) +} +func testConnCloseUnblocksStreamRead(t *testing.T) { testConnCloseUnblocks(t, func(ctx context.Context, tc *testConn) error { s := newLocalStream(t, tc, bidiStream) s.SetReadContext(ctx) @@ -255,6 +285,9 @@ func TestConnCloseUnblocksStreamRead(t *testing.T) { } func TestConnCloseUnblocksStreamWrite(t *testing.T) { + synctest.Test(t, testConnCloseUnblocksStreamWrite) +} +func testConnCloseUnblocksStreamWrite(t *testing.T) { testConnCloseUnblocks(t, func(ctx context.Context, tc *testConn) error { s := newLocalStream(t, tc, bidiStream) s.SetWriteContext(ctx) @@ -267,6 +300,9 @@ func TestConnCloseUnblocksStreamWrite(t *testing.T) { } func TestConnCloseUnblocksStreamClose(t *testing.T) { + synctest.Test(t, testConnCloseUnblocksStreamClose) +} +func testConnCloseUnblocksStreamClose(t *testing.T) { testConnCloseUnblocks(t, func(ctx context.Context, tc *testConn) error { s := newLocalStream(t, tc, bidiStream) s.SetWriteContext(ctx) diff --git a/quic/conn_flow_test.go b/quic/conn_flow_test.go index 52ecf922..d8d3ae76 100644 --- a/quic/conn_flow_test.go +++ b/quic/conn_flow_test.go @@ -2,14 +2,20 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +//go:build go1.25 + package quic import ( "context" "testing" + "testing/synctest" ) func TestConnInflowReturnOnRead(t *testing.T) { + synctest.Test(t, testConnInflowReturnOnRead) +} +func testConnInflowReturnOnRead(t *testing.T) { tc, s := newTestConnAndRemoteStream(t, serverSide, uniStream, func(c *Config) { c.MaxConnReadBufferSize = 64 }) @@ -41,6 +47,9 @@ func TestConnInflowReturnOnRead(t *testing.T) { } func TestConnInflowReturnOnRacingReads(t *testing.T) { + synctest.Test(t, testConnInflowReturnOnRacingReads) +} +func testConnInflowReturnOnRacingReads(t *testing.T) { // Perform two reads at the same time, // one for half of MaxConnReadBufferSize // and one for one byte. @@ -91,6 +100,9 @@ func TestConnInflowReturnOnRacingReads(t *testing.T) { } func TestConnInflowReturnOnClose(t *testing.T) { + synctest.Test(t, testConnInflowReturnOnClose) +} +func testConnInflowReturnOnClose(t *testing.T) { tc, s := newTestConnAndRemoteStream(t, serverSide, uniStream, func(c *Config) { c.MaxConnReadBufferSize = 64 }) @@ -107,6 +119,9 @@ func TestConnInflowReturnOnClose(t *testing.T) { } func TestConnInflowReturnOnReset(t *testing.T) { + synctest.Test(t, testConnInflowReturnOnReset) +} +func testConnInflowReturnOnReset(t *testing.T) { tc, s := newTestConnAndRemoteStream(t, serverSide, uniStream, func(c *Config) { c.MaxConnReadBufferSize = 64 }) @@ -127,6 +142,9 @@ func TestConnInflowReturnOnReset(t *testing.T) { } func TestConnInflowStreamViolation(t *testing.T) { + synctest.Test(t, testConnInflowStreamViolation) +} +func testConnInflowStreamViolation(t *testing.T) { tc := newTestConn(t, serverSide, func(c *Config) { c.MaxConnReadBufferSize = 100 }) @@ -169,6 +187,9 @@ func TestConnInflowStreamViolation(t *testing.T) { } func TestConnInflowResetViolation(t *testing.T) { + synctest.Test(t, testConnInflowResetViolation) +} +func testConnInflowResetViolation(t *testing.T) { tc := newTestConn(t, serverSide, func(c *Config) { c.MaxConnReadBufferSize = 100 }) @@ -197,6 +218,9 @@ func TestConnInflowResetViolation(t *testing.T) { } func TestConnInflowMultipleStreams(t *testing.T) { + synctest.Test(t, testConnInflowMultipleStreams) +} +func testConnInflowMultipleStreams(t *testing.T) { tc := newTestConn(t, serverSide, func(c *Config) { c.MaxConnReadBufferSize = 128 }) @@ -247,6 +271,9 @@ func TestConnInflowMultipleStreams(t *testing.T) { } func TestConnOutflowBlocked(t *testing.T) { + synctest.Test(t, testConnOutflowBlocked) +} +func testConnOutflowBlocked(t *testing.T) { tc, s := newTestConnAndLocalStream(t, clientSide, uniStream, permissiveTransportParameters, func(p *transportParameters) { @@ -291,6 +318,9 @@ func TestConnOutflowBlocked(t *testing.T) { } func TestConnOutflowMaxDataDecreases(t *testing.T) { + synctest.Test(t, testConnOutflowMaxDataDecreases) +} +func testConnOutflowMaxDataDecreases(t *testing.T) { tc, s := newTestConnAndLocalStream(t, clientSide, uniStream, permissiveTransportParameters, func(p *transportParameters) { @@ -318,6 +348,9 @@ func TestConnOutflowMaxDataDecreases(t *testing.T) { } func TestConnOutflowMaxDataRoundRobin(t *testing.T) { + synctest.Test(t, testConnOutflowMaxDataRoundRobin) +} +func testConnOutflowMaxDataRoundRobin(t *testing.T) { ctx := canceledContext() tc := newTestConn(t, clientSide, permissiveTransportParameters, func(p *transportParameters) { @@ -370,6 +403,9 @@ func TestConnOutflowMaxDataRoundRobin(t *testing.T) { } func TestConnOutflowMetaAndData(t *testing.T) { + synctest.Test(t, testConnOutflowMetaAndData) +} +func testConnOutflowMetaAndData(t *testing.T) { tc, s := newTestConnAndLocalStream(t, clientSide, bidiStream, permissiveTransportParameters, func(p *transportParameters) { @@ -398,6 +434,9 @@ func TestConnOutflowMetaAndData(t *testing.T) { } func TestConnOutflowResentData(t *testing.T) { + synctest.Test(t, testConnOutflowResentData) +} +func testConnOutflowResentData(t *testing.T) { tc, s := newTestConnAndLocalStream(t, clientSide, bidiStream, permissiveTransportParameters, func(p *transportParameters) { diff --git a/quic/conn_id_test.go b/quic/conn_id_test.go index c9da0eb0..4b4da675 100644 --- a/quic/conn_id_test.go +++ b/quic/conn_id_test.go @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +//go:build go1.25 + package quic import ( @@ -11,9 +13,13 @@ import ( "net/netip" "strings" "testing" + "testing/synctest" ) func TestConnIDClientHandshake(t *testing.T) { + synctest.Test(t, testConnIDClientHandshake) +} +func testConnIDClientHandshake(t *testing.T) { tc := newTestConn(t, clientSide) // On initialization, the client chooses local and remote IDs. // @@ -57,6 +63,9 @@ func TestConnIDClientHandshake(t *testing.T) { } func TestConnIDServerHandshake(t *testing.T) { + synctest.Test(t, testConnIDServerHandshake) +} +func testConnIDServerHandshake(t *testing.T) { tc := newTestConn(t, serverSide) // On initialization, the server is provided with the client-chosen // transient connection ID, and allocates an ID of its own. @@ -178,6 +187,9 @@ func TestNewRandomConnID(t *testing.T) { } func TestConnIDPeerRequestsManyIDs(t *testing.T) { + synctest.Test(t, testConnIDPeerRequestsManyIDs) +} +func testConnIDPeerRequestsManyIDs(t *testing.T) { // "An endpoint SHOULD ensure that its peer has a sufficient number // of available and unused connection IDs." // https://www.rfc-editor.org/rfc/rfc9000#section-5.1.1-4 @@ -220,6 +232,9 @@ func TestConnIDPeerRequestsManyIDs(t *testing.T) { } func TestConnIDPeerProvidesTooManyIDs(t *testing.T) { + synctest.Test(t, testConnIDPeerProvidesTooManyIDs) +} +func testConnIDPeerProvidesTooManyIDs(t *testing.T) { // "An endpoint MUST NOT provide more connection IDs than the peer's limit." // https://www.rfc-editor.org/rfc/rfc9000#section-5.1.1-4 tc := newTestConn(t, serverSide) @@ -238,6 +253,9 @@ func TestConnIDPeerProvidesTooManyIDs(t *testing.T) { } func TestConnIDPeerTemporarilyExceedsActiveConnIDLimit(t *testing.T) { + synctest.Test(t, testConnIDPeerTemporarilyExceedsActiveConnIDLimit) +} +func testConnIDPeerTemporarilyExceedsActiveConnIDLimit(t *testing.T) { // "An endpoint MAY send connection IDs that temporarily exceed a peer's limit // if the NEW_CONNECTION_ID frame also requires the retirement of any excess [...]" // https://www.rfc-editor.org/rfc/rfc9000#section-5.1.1-4 @@ -272,7 +290,7 @@ func TestConnIDPeerRetiresConnID(t *testing.T) { clientSide, serverSide, } { - t.Run(side.String(), func(t *testing.T) { + synctestSubtest(t, side.String(), func(t *testing.T) { tc := newTestConn(t, side) tc.handshake() tc.ignoreFrame(frameTypeAck) @@ -293,6 +311,9 @@ func TestConnIDPeerRetiresConnID(t *testing.T) { } func TestConnIDPeerWithZeroLengthConnIDSendsNewConnectionID(t *testing.T) { + synctest.Test(t, testConnIDPeerWithZeroLengthConnIDSendsNewConnectionID) +} +func testConnIDPeerWithZeroLengthConnIDSendsNewConnectionID(t *testing.T) { // "An endpoint that selects a zero-length connection ID during the handshake // cannot issue a new connection ID." // https://www.rfc-editor.org/rfc/rfc9000#section-5.1.1-8 @@ -315,6 +336,9 @@ func TestConnIDPeerWithZeroLengthConnIDSendsNewConnectionID(t *testing.T) { } func TestConnIDPeerRequestsRetirement(t *testing.T) { + synctest.Test(t, testConnIDPeerRequestsRetirement) +} +func testConnIDPeerRequestsRetirement(t *testing.T) { // "Upon receipt of an increased Retire Prior To field, the peer MUST // stop using the corresponding connection IDs and retire them with // RETIRE_CONNECTION_ID frames [...]" @@ -339,6 +363,9 @@ func TestConnIDPeerRequestsRetirement(t *testing.T) { } func TestConnIDPeerDoesNotAcknowledgeRetirement(t *testing.T) { + synctest.Test(t, testConnIDPeerDoesNotAcknowledgeRetirement) +} +func testConnIDPeerDoesNotAcknowledgeRetirement(t *testing.T) { // "An endpoint SHOULD limit the number of connection IDs it has retired locally // for which RETIRE_CONNECTION_ID frames have not yet been acknowledged." // https://www.rfc-editor.org/rfc/rfc9000#section-5.1.2-6 @@ -364,6 +391,9 @@ func TestConnIDPeerDoesNotAcknowledgeRetirement(t *testing.T) { } func TestConnIDRepeatedNewConnectionIDFrame(t *testing.T) { + synctest.Test(t, testConnIDRepeatedNewConnectionIDFrame) +} +func testConnIDRepeatedNewConnectionIDFrame(t *testing.T) { // "Receipt of the same [NEW_CONNECTION_ID] frame multiple times // MUST NOT be treated as a connection error. // https://www.rfc-editor.org/rfc/rfc9000#section-19.15-7 @@ -387,6 +417,9 @@ func TestConnIDRepeatedNewConnectionIDFrame(t *testing.T) { } func TestConnIDForSequenceNumberChanges(t *testing.T) { + synctest.Test(t, testConnIDForSequenceNumberChanges) +} +func testConnIDForSequenceNumberChanges(t *testing.T) { // "[...] if a sequence number is used for different connection IDs, // the endpoint MAY treat that receipt as a connection error // of type PROTOCOL_VIOLATION." @@ -415,6 +448,9 @@ func TestConnIDForSequenceNumberChanges(t *testing.T) { } func TestConnIDRetirePriorToAfterNewConnID(t *testing.T) { + synctest.Test(t, testConnIDRetirePriorToAfterNewConnID) +} +func testConnIDRetirePriorToAfterNewConnID(t *testing.T) { // "Receiving a value in the Retire Prior To field that is greater than // that in the Sequence Number field MUST be treated as a connection error // of type FRAME_ENCODING_ERROR. @@ -436,6 +472,9 @@ func TestConnIDRetirePriorToAfterNewConnID(t *testing.T) { } func TestConnIDAlreadyRetired(t *testing.T) { + synctest.Test(t, testConnIDAlreadyRetired) +} +func testConnIDAlreadyRetired(t *testing.T) { // "An endpoint that receives a NEW_CONNECTION_ID frame with a // sequence number smaller than the Retire Prior To field of a // previously received NEW_CONNECTION_ID frame MUST send a @@ -472,6 +511,9 @@ func TestConnIDAlreadyRetired(t *testing.T) { } func TestConnIDRepeatedRetireConnectionIDFrame(t *testing.T) { + synctest.Test(t, testConnIDRepeatedRetireConnectionIDFrame) +} +func testConnIDRepeatedRetireConnectionIDFrame(t *testing.T) { tc := newTestConn(t, clientSide) tc.handshake() tc.ignoreFrame(frameTypeAck) @@ -493,6 +535,9 @@ func TestConnIDRepeatedRetireConnectionIDFrame(t *testing.T) { } func TestConnIDRetiredUnsent(t *testing.T) { + synctest.Test(t, testConnIDRetiredUnsent) +} +func testConnIDRetiredUnsent(t *testing.T) { // "Receipt of a RETIRE_CONNECTION_ID frame containing a sequence number // greater than any previously sent to the peer MUST be treated as a // connection error of type PROTOCOL_VIOLATION." @@ -512,6 +557,9 @@ func TestConnIDRetiredUnsent(t *testing.T) { } func TestConnIDUsePreferredAddressConnID(t *testing.T) { + synctest.Test(t, testConnIDUsePreferredAddressConnID) +} +func testConnIDUsePreferredAddressConnID(t *testing.T) { // Peer gives us a connection ID in the preferred address transport parameter. // We don't use the preferred address at this time, but we should use the // connection ID. (It isn't tied to any specific address.) @@ -543,6 +591,9 @@ func TestConnIDUsePreferredAddressConnID(t *testing.T) { } func TestConnIDPeerProvidesPreferredAddrAndTooManyConnIDs(t *testing.T) { + synctest.Test(t, testConnIDPeerProvidesPreferredAddrAndTooManyConnIDs) +} +func testConnIDPeerProvidesPreferredAddrAndTooManyConnIDs(t *testing.T) { // Peer gives us more conn ids than our advertised limit, // including a conn id in the preferred address transport parameter. cid := testPeerConnID(10) @@ -568,6 +619,9 @@ func TestConnIDPeerProvidesPreferredAddrAndTooManyConnIDs(t *testing.T) { } func TestConnIDPeerWithZeroLengthIDProvidesPreferredAddr(t *testing.T) { + synctest.Test(t, testConnIDPeerWithZeroLengthIDProvidesPreferredAddr) +} +func testConnIDPeerWithZeroLengthIDProvidesPreferredAddr(t *testing.T) { // Peer gives us more conn ids than our advertised limit, // including a conn id in the preferred address transport parameter. tc := newTestConn(t, serverSide, func(p *transportParameters) { @@ -596,7 +650,7 @@ func TestConnIDInitialSrcConnIDMismatch(t *testing.T) { // "Endpoints MUST validate that received [initial_source_connection_id] // parameters match received connection ID values." // https://www.rfc-editor.org/rfc/rfc9000#section-7.3-3 - testSides(t, "", func(t *testing.T, side connSide) { + testSidesSynctest(t, "", func(t *testing.T, side connSide) { tc := newTestConn(t, side, func(p *transportParameters) { p.initialSrcConnID = []byte("invalid") }) @@ -621,7 +675,7 @@ func TestConnIDInitialSrcConnIDMismatch(t *testing.T) { } func TestConnIDsCleanedUpAfterClose(t *testing.T) { - testSides(t, "", func(t *testing.T, side connSide) { + testSidesSynctest(t, "", func(t *testing.T, side connSide) { tc := newTestConn(t, side, func(p *transportParameters) { if side == clientSide { token := testPeerStatelessResetToken(0) @@ -664,6 +718,9 @@ func TestConnIDsCleanedUpAfterClose(t *testing.T) { } func TestConnIDRetiredConnIDResent(t *testing.T) { + synctest.Test(t, testConnIDRetiredConnIDResent) +} +func testConnIDRetiredConnIDResent(t *testing.T) { tc := newTestConn(t, serverSide) tc.handshake() tc.ignoreFrame(frameTypeAck) diff --git a/quic/conn_loss_test.go b/quic/conn_loss_test.go index f13ea13d..49c794ff 100644 --- a/quic/conn_loss_test.go +++ b/quic/conn_loss_test.go @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +//go:build go1.25 + package quic import ( @@ -9,6 +11,8 @@ import ( "crypto/tls" "fmt" "testing" + "testing/synctest" + "time" ) // Frames may be retransmitted either when the packet containing the frame is lost, or on PTO. @@ -22,6 +26,16 @@ func lostFrameTest(t *testing.T, f func(t *testing.T, pto bool)) { }) } +func lostFrameTestSynctest(t *testing.T, f func(t *testing.T, pto bool)) { + t.Helper() + lostFrameTest(t, func(t *testing.T, pto bool) { + t.Helper() + synctest.Test(t, func(t *testing.T) { + f(t, pto) + }) + }) +} + // triggerLossOrPTO causes the conn to declare the last sent packet lost, // or advances to the PTO timer. func (tc *testConn) triggerLossOrPTO(ptype packetType, pto bool) { @@ -33,7 +47,11 @@ func (tc *testConn) triggerLossOrPTO(ptype packetType, pto bool) { if *testVV { tc.t.Logf("advancing to PTO timer") } - tc.advanceTo(tc.conn.loss.timer) + var when time.Time + tc.conn.runOnLoop(tc.t.Context(), func(now time.Time, conn *Conn) { + when = conn.loss.timer + }) + time.Sleep(time.Until(when)) return } if *testVV { @@ -77,7 +95,7 @@ func TestLostResetStreamFrame(t *testing.T) { // "Cancellation of stream transmission, as carried in a RESET_STREAM frame, // is sent until acknowledged or until all stream data is acknowledged by the peer [...]" // https://www.rfc-editor.org/rfc/rfc9000.html#section-13.3-3.4 - lostFrameTest(t, func(t *testing.T, pto bool) { + lostFrameTestSynctest(t, func(t *testing.T, pto bool) { tc, s := newTestConnAndLocalStream(t, serverSide, uniStream, permissiveTransportParameters) tc.ignoreFrame(frameTypeAck) @@ -106,7 +124,7 @@ func TestLostStopSendingFrame(t *testing.T) { // Technically, we can stop sending a STOP_SENDING frame if the peer sends // us all the data for the stream or resets it. We don't bother tracking this, // however, so we'll keep sending the frame until it is acked. This is harmless. - lostFrameTest(t, func(t *testing.T, pto bool) { + lostFrameTestSynctest(t, func(t *testing.T, pto bool) { tc, s := newTestConnAndRemoteStream(t, serverSide, uniStream, permissiveTransportParameters) tc.ignoreFrame(frameTypeAck) @@ -127,7 +145,7 @@ func TestLostStopSendingFrame(t *testing.T) { func TestLostCryptoFrame(t *testing.T) { // "Data sent in CRYPTO frames is retransmitted [...] until all data has been acknowledged." // https://www.rfc-editor.org/rfc/rfc9000.html#section-13.3-3.1 - lostFrameTest(t, func(t *testing.T, pto bool) { + lostFrameTestSynctest(t, func(t *testing.T, pto bool) { tc := newTestConn(t, clientSide) tc.ignoreFrame(frameTypeAck) @@ -171,7 +189,7 @@ func TestLostCryptoFrame(t *testing.T) { func TestLostStreamFrameEmpty(t *testing.T) { // A STREAM frame opening a stream, but containing no stream data, should // be retransmitted if lost. - lostFrameTest(t, func(t *testing.T, pto bool) { + lostFrameTestSynctest(t, func(t *testing.T, pto bool) { ctx := canceledContext() tc := newTestConn(t, clientSide, permissiveTransportParameters) tc.handshake() @@ -203,7 +221,7 @@ func TestLostStreamWithData(t *testing.T) { // https://www.rfc-editor.org/rfc/rfc9000#section-13.3-3.2 // // TODO: Lost stream frame after RESET_STREAM - lostFrameTest(t, func(t *testing.T, pto bool) { + lostFrameTestSynctest(t, func(t *testing.T, pto bool) { data := []byte{0, 1, 2, 3, 4, 5, 6, 7} tc, s := newTestConnAndLocalStream(t, serverSide, uniStream, func(p *transportParameters) { p.initialMaxStreamsUni = 1 @@ -247,6 +265,9 @@ func TestLostStreamWithData(t *testing.T) { } func TestLostStreamPartialLoss(t *testing.T) { + synctest.Test(t, testLostStreamPartialLoss) +} +func testLostStreamPartialLoss(t *testing.T) { // Conn sends four STREAM packets. // ACKs are received for the packets containing bytes 0 and 2. // The remaining packets are declared lost. @@ -295,7 +316,7 @@ func TestLostMaxDataFrame(t *testing.T) { // "An updated value is sent in a MAX_DATA frame if the packet // containing the most recently sent MAX_DATA frame is declared lost [...]" // https://www.rfc-editor.org/rfc/rfc9000#section-13.3-3.7 - lostFrameTest(t, func(t *testing.T, pto bool) { + lostFrameTestSynctest(t, func(t *testing.T, pto bool) { const maxWindowSize = 32 buf := make([]byte, maxWindowSize) tc, s := newTestConnAndRemoteStream(t, serverSide, uniStream, func(c *Config) { @@ -340,7 +361,7 @@ func TestLostMaxStreamDataFrame(t *testing.T) { // "[...] an updated value is sent when the packet containing // the most recent MAX_STREAM_DATA frame for a stream is lost" // https://www.rfc-editor.org/rfc/rfc9000#section-13.3-3.8 - lostFrameTest(t, func(t *testing.T, pto bool) { + lostFrameTestSynctest(t, func(t *testing.T, pto bool) { const maxWindowSize = 32 buf := make([]byte, maxWindowSize) tc, s := newTestConnAndRemoteStream(t, serverSide, uniStream, func(c *Config) { @@ -387,7 +408,7 @@ func TestLostMaxStreamDataFrameAfterStreamFinReceived(t *testing.T) { // "An endpoint SHOULD stop sending MAX_STREAM_DATA frames when // the receiving part of the stream enters a "Size Known" or "Reset Recvd" state." // https://www.rfc-editor.org/rfc/rfc9000#section-13.3-3.8 - lostFrameTest(t, func(t *testing.T, pto bool) { + lostFrameTestSynctest(t, func(t *testing.T, pto bool) { const maxWindowSize = 10 buf := make([]byte, maxWindowSize) tc, s := newTestConnAndRemoteStream(t, serverSide, uniStream, func(c *Config) { @@ -425,7 +446,7 @@ func TestLostMaxStreamsFrameMostRecent(t *testing.T) { // most recent MAX_STREAMS for a stream type frame is declared lost [...]" // https://www.rfc-editor.org/rfc/rfc9000#section-13.3-3.9 testStreamTypes(t, "", func(t *testing.T, styp streamType) { - lostFrameTest(t, func(t *testing.T, pto bool) { + lostFrameTestSynctest(t, func(t *testing.T, pto bool) { ctx := canceledContext() tc := newTestConn(t, serverSide, func(c *Config) { c.MaxUniRemoteStreams = 1 @@ -469,6 +490,9 @@ func TestLostMaxStreamsFrameMostRecent(t *testing.T) { } func TestLostMaxStreamsFrameNotMostRecent(t *testing.T) { + synctest.Test(t, testLostMaxStreamsFrameNotMostRecent) +} +func testLostMaxStreamsFrameNotMostRecent(t *testing.T) { // Send two MAX_STREAMS frames, lose the first one. // // No PTO mode for this test: The ack that causes the first frame @@ -514,7 +538,7 @@ func TestLostStreamDataBlockedFrame(t *testing.T) { // "A new [STREAM_DATA_BLOCKED] frame is sent if a packet containing // the most recent frame for a scope is lost [...]" // https://www.rfc-editor.org/rfc/rfc9000#section-13.3-3.10 - lostFrameTest(t, func(t *testing.T, pto bool) { + lostFrameTestSynctest(t, func(t *testing.T, pto bool) { tc, s := newTestConnAndLocalStream(t, serverSide, uniStream, func(p *transportParameters) { p.initialMaxStreamsUni = 1 p.initialMaxData = 1 << 20 @@ -565,7 +589,7 @@ func TestLostStreamDataBlockedFrameAfterStreamUnblocked(t *testing.T) { // "A new [STREAM_DATA_BLOCKED] frame is sent [...] only while // the endpoint is blocked on the corresponding limit." // https://www.rfc-editor.org/rfc/rfc9000#section-13.3-3.10 - lostFrameTest(t, func(t *testing.T, pto bool) { + lostFrameTestSynctest(t, func(t *testing.T, pto bool) { tc, s := newTestConnAndLocalStream(t, serverSide, uniStream, func(p *transportParameters) { p.initialMaxStreamsUni = 1 p.initialMaxData = 1 << 20 @@ -607,7 +631,7 @@ func TestLostStreamDataBlockedFrameAfterStreamUnblocked(t *testing.T) { func TestLostNewConnectionIDFrame(t *testing.T) { // "New connection IDs are [...] retransmitted if the packet containing them is lost." // https://www.rfc-editor.org/rfc/rfc9000#section-13.3-3.13 - lostFrameTest(t, func(t *testing.T, pto bool) { + lostFrameTestSynctest(t, func(t *testing.T, pto bool) { tc := newTestConn(t, serverSide) tc.handshake() tc.ignoreFrame(frameTypeAck) @@ -637,7 +661,7 @@ func TestLostRetireConnectionIDFrame(t *testing.T) { // "[...] retired connection IDs are [...] retransmitted // if the packet containing them is lost." // https://www.rfc-editor.org/rfc/rfc9000#section-13.3-3.13 - lostFrameTest(t, func(t *testing.T, pto bool) { + lostFrameTestSynctest(t, func(t *testing.T, pto bool) { tc := newTestConn(t, clientSide) tc.handshake() tc.ignoreFrame(frameTypeAck) @@ -664,7 +688,7 @@ func TestLostRetireConnectionIDFrame(t *testing.T) { func TestLostPathResponseFrame(t *testing.T) { // "Responses to path validation using PATH_RESPONSE frames are sent just once." // https://www.rfc-editor.org/rfc/rfc9000.html#section-13.3-3.12 - lostFrameTest(t, func(t *testing.T, pto bool) { + lostFrameTestSynctest(t, func(t *testing.T, pto bool) { tc := newTestConn(t, clientSide) tc.handshake() tc.ignoreFrame(frameTypeAck) @@ -687,7 +711,7 @@ func TestLostPathResponseFrame(t *testing.T) { func TestLostHandshakeDoneFrame(t *testing.T) { // "The HANDSHAKE_DONE frame MUST be retransmitted until it is acknowledged." // https://www.rfc-editor.org/rfc/rfc9000.html#section-13.3-3.16 - lostFrameTest(t, func(t *testing.T, pto bool) { + lostFrameTestSynctest(t, func(t *testing.T, pto bool) { tc := newTestConn(t, serverSide) tc.ignoreFrame(frameTypeAck) diff --git a/quic/conn_recv_test.go b/quic/conn_recv_test.go index 1a0eb3a1..6ee728e0 100644 --- a/quic/conn_recv_test.go +++ b/quic/conn_recv_test.go @@ -2,14 +2,20 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +//go:build go1.25 + package quic import ( "crypto/tls" "testing" + "testing/synctest" ) func TestConnReceiveAckForUnsentPacket(t *testing.T) { + synctest.Test(t, testConnReceiveAckForUnsentPacket) +} +func testConnReceiveAckForUnsentPacket(t *testing.T) { tc := newTestConn(t, serverSide, permissiveTransportParameters) tc.handshake() tc.writeFrames(packetType1RTT, @@ -27,6 +33,9 @@ func TestConnReceiveAckForUnsentPacket(t *testing.T) { // drop state for a number space, and also contains a valid ACK frame for that space, // we shouldn't complain about the ACK. func TestConnReceiveAckForDroppedSpace(t *testing.T) { + synctest.Test(t, testConnReceiveAckForDroppedSpace) +} +func testConnReceiveAckForDroppedSpace(t *testing.T) { tc := newTestConn(t, serverSide, permissiveTransportParameters) tc.ignoreFrame(frameTypeAck) tc.ignoreFrame(frameTypeNewConnectionID) diff --git a/quic/conn_send_test.go b/quic/conn_send_test.go index c5cf9364..88911bd1 100644 --- a/quic/conn_send_test.go +++ b/quic/conn_send_test.go @@ -2,14 +2,20 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +//go:build go1.25 + package quic import ( "testing" + "testing/synctest" "time" ) func TestAckElicitingAck(t *testing.T) { + synctest.Test(t, testAckElicitingAck) +} +func testAckElicitingAck(t *testing.T) { // "A receiver that sends only non-ack-eliciting packets [...] might not receive // an acknowledgment for a long period of time. // [...] a receiver could send a [...] ack-eliciting frame occasionally [...] @@ -22,7 +28,7 @@ func TestAckElicitingAck(t *testing.T) { tc.handshake() const count = 100 for i := 0; i < count; i++ { - tc.advance(1 * time.Millisecond) + time.Sleep(1 * time.Millisecond) tc.writeFrames(packetType1RTT, debugFramePing{}, ) @@ -38,6 +44,9 @@ func TestAckElicitingAck(t *testing.T) { } func TestSendPacketNumberSize(t *testing.T) { + synctest.Test(t, testSendPacketNumberSize) +} +func testSendPacketNumberSize(t *testing.T) { tc := newTestConn(t, clientSide, permissiveTransportParameters) tc.handshake() diff --git a/quic/conn_streams.go b/quic/conn_streams.go index 80884fd6..0e4bf500 100644 --- a/quic/conn_streams.go +++ b/quic/conn_streams.go @@ -71,7 +71,7 @@ func (c *Conn) streamsCleanup() { // AcceptStream waits for and returns the next stream created by the peer. func (c *Conn) AcceptStream(ctx context.Context) (*Stream, error) { - return c.streams.queue.get(ctx, c.testHooks) + return c.streams.queue.get(ctx) } // NewStream creates a stream. diff --git a/quic/conn_streams_test.go b/quic/conn_streams_test.go index af3c1dec..b95aa471 100644 --- a/quic/conn_streams_test.go +++ b/quic/conn_streams_test.go @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +//go:build go1.25 + package quic import ( @@ -11,9 +13,13 @@ import ( "math" "sync" "testing" + "testing/synctest" ) func TestStreamsCreate(t *testing.T) { + synctest.Test(t, testStreamsCreate) +} +func testStreamsCreate(t *testing.T) { ctx := canceledContext() tc := newTestConn(t, clientSide, permissiveTransportParameters) tc.handshake() @@ -53,6 +59,9 @@ func TestStreamsCreate(t *testing.T) { } func TestStreamsAccept(t *testing.T) { + synctest.Test(t, testStreamsAccept) +} +func testStreamsAccept(t *testing.T) { ctx := canceledContext() tc := newTestConn(t, serverSide) tc.handshake() @@ -95,6 +104,9 @@ func TestStreamsAccept(t *testing.T) { } func TestStreamsBlockingAccept(t *testing.T) { + synctest.Test(t, testStreamsBlockingAccept) +} +func testStreamsBlockingAccept(t *testing.T) { tc := newTestConn(t, serverSide) tc.handshake() @@ -124,6 +136,9 @@ func TestStreamsBlockingAccept(t *testing.T) { } func TestStreamsLocalStreamNotCreated(t *testing.T) { + synctest.Test(t, testStreamsLocalStreamNotCreated) +} +func testStreamsLocalStreamNotCreated(t *testing.T) { // "An endpoint MUST terminate the connection with error STREAM_STATE_ERROR // if it receives a STREAM frame for a locally initiated stream that has // not yet been created [...]" @@ -142,6 +157,9 @@ func TestStreamsLocalStreamNotCreated(t *testing.T) { } func TestStreamsLocalStreamClosed(t *testing.T) { + synctest.Test(t, testStreamsLocalStreamClosed) +} +func testStreamsLocalStreamClosed(t *testing.T) { tc, s := newTestConnAndLocalStream(t, clientSide, uniStream, permissiveTransportParameters) s.CloseWrite() tc.wantFrame("FIN for closed stream", @@ -168,6 +186,9 @@ func TestStreamsLocalStreamClosed(t *testing.T) { } func TestStreamsStreamSendOnly(t *testing.T) { + synctest.Test(t, testStreamsStreamSendOnly) +} +func testStreamsStreamSendOnly(t *testing.T) { // "An endpoint MUST terminate the connection with error STREAM_STATE_ERROR // if it receives a STREAM frame for a locally initiated stream that has // not yet been created [...]" @@ -198,6 +219,9 @@ func TestStreamsStreamSendOnly(t *testing.T) { } func TestStreamsWriteQueueFairness(t *testing.T) { + synctest.Test(t, testStreamsWriteQueueFairness) +} +func testStreamsWriteQueueFairness(t *testing.T) { ctx := canceledContext() const dataLen = 1 << 20 const numStreams = 3 @@ -233,7 +257,7 @@ func TestStreamsWriteQueueFairness(t *testing.T) { } // Wait for the stream to finish writing whatever frames it can before // congestion control blocks it. - tc.wait() + synctest.Wait() } sent := make([]int64, len(streams)) @@ -344,7 +368,7 @@ func TestStreamsShutdown(t *testing.T) { }, }} { name := fmt.Sprintf("%v/%v/%v", test.side, test.styp, test.name) - t.Run(name, func(t *testing.T) { + synctestSubtest(t, name, func(t *testing.T) { tc, s := newTestConnAndStream(t, serverSide, test.side, test.styp, permissiveTransportParameters) tc.ignoreFrame(frameTypeStreamBase) @@ -364,6 +388,9 @@ func TestStreamsShutdown(t *testing.T) { } func TestStreamsCreateAndCloseRemote(t *testing.T) { + synctest.Test(t, testStreamsCreateAndCloseRemote) +} +func testStreamsCreateAndCloseRemote(t *testing.T) { // This test exercises creating new streams in response to frames // from the peer, and cleaning up after streams are fully closed. // @@ -473,6 +500,9 @@ func TestStreamsCreateAndCloseRemote(t *testing.T) { } func TestStreamsCreateConcurrency(t *testing.T) { + synctest.Test(t, testStreamsCreateConcurrency) +} +func testStreamsCreateConcurrency(t *testing.T) { cli, srv := newLocalConnPair(t, &Config{}, &Config{}) srvdone := make(chan int) @@ -520,6 +550,9 @@ func TestStreamsCreateConcurrency(t *testing.T) { } func TestStreamsPTOWithImplicitStream(t *testing.T) { + synctest.Test(t, testStreamsPTOWithImplicitStream) +} +func testStreamsPTOWithImplicitStream(t *testing.T) { ctx := canceledContext() tc := newTestConn(t, serverSide, permissiveTransportParameters) tc.handshake() diff --git a/quic/conn_test.go b/quic/conn_test.go index a5f2f611..81eeffc5 100644 --- a/quic/conn_test.go +++ b/quic/conn_test.go @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +//go:build go1.25 + package quic import ( @@ -17,6 +19,7 @@ import ( "reflect" "strings" "testing" + "testing/synctest" "time" "golang.org/x/net/quic/qlog" @@ -27,7 +30,8 @@ var ( qlogdir = flag.String("qlog", "", "write qlog logs to directory") ) -func TestConnTestConn(t *testing.T) { +func TestConnTestConn(t *testing.T) { synctest.Test(t, testConnTestConn) } +func testConnTestConn(t *testing.T) { tc := newTestConn(t, serverSide) tc.handshake() if got, want := tc.timeUntilEvent(), defaultMaxIdleTimeout; got != want { @@ -40,13 +44,13 @@ func TestConnTestConn(t *testing.T) { }) return }).result() - if !ranAt.Equal(tc.endpoint.now) { - t.Errorf("func ran on loop at %v, want %v", ranAt, tc.endpoint.now) + if !ranAt.Equal(time.Now()) { + t.Errorf("func ran on loop at %v, want %v", ranAt, time.Now()) } - tc.wait() + synctest.Wait() - nextTime := tc.endpoint.now.Add(defaultMaxIdleTimeout / 2) - tc.advanceTo(nextTime) + nextTime := time.Now().Add(defaultMaxIdleTimeout / 2) + time.Sleep(time.Until(nextTime)) ranAt, _ = runAsync(tc, func(ctx context.Context) (when time.Time, _ error) { tc.conn.runOnLoop(ctx, func(now time.Time, c *Conn) { when = now @@ -56,7 +60,7 @@ func TestConnTestConn(t *testing.T) { if !ranAt.Equal(nextTime) { t.Errorf("func ran on loop at %v, want %v", ranAt, nextTime) } - tc.wait() + synctest.Wait() tc.advanceToTimer() if got := tc.conn.lifetime.state; got != connStateDone { @@ -125,12 +129,9 @@ const maxTestKeyPhases = 3 // A testConn is a Conn whose external interactions (sending and receiving packets, // setting timers) can be manipulated in tests. type testConn struct { - t *testing.T - conn *Conn - endpoint *testEndpoint - timer time.Time - timerLastFired time.Time - idlec chan struct{} // only accessed on the conn's loop + t *testing.T + conn *Conn + endpoint *testEndpoint // Keys are distinct from the conn's keys, // because the test may know about keys before the conn does. @@ -183,8 +184,6 @@ type testConn struct { // Values to set in packets sent to the conn. sendKeyNumber int sendKeyPhaseBit bool - - asyncTestState } type test1RTTKeys struct { @@ -198,10 +197,6 @@ type keySecret struct { } // newTestConn creates a Conn for testing. -// -// The Conn's event loop is controlled by the test, -// allowing test code to access Conn state directly -// by first ensuring the loop goroutine is idle. func newTestConn(t *testing.T, side connSide, opts ...any) *testConn { t.Helper() config := &Config{ @@ -242,7 +237,7 @@ func newTestConn(t *testing.T, side connSide, opts ...any) *testConn { endpoint.configTransportParams = configTransportParams endpoint.configTestConn = configTestConn conn, err := endpoint.e.newConn( - endpoint.now, + time.Now(), config, side, cids, @@ -252,7 +247,7 @@ func newTestConn(t *testing.T, side connSide, opts ...any) *testConn { t.Fatal(err) } tc := endpoint.conns[conn] - tc.wait() + synctest.Wait() return tc } @@ -306,76 +301,33 @@ func newTestConnForConn(t *testing.T, endpoint *testEndpoint, conn *Conn) *testC return tc } -// advance causes time to pass. -func (tc *testConn) advance(d time.Duration) { - tc.t.Helper() - tc.endpoint.advance(d) -} - -// advanceTo sets the current time. -func (tc *testConn) advanceTo(now time.Time) { - tc.t.Helper() - tc.endpoint.advanceTo(now) -} - // advanceToTimer sets the current time to the time of the Conn's next timer event. func (tc *testConn) advanceToTimer() { - if tc.timer.IsZero() { + when := tc.nextEvent() + if when.IsZero() { tc.t.Fatalf("advancing to timer, but timer is not set") } - tc.advanceTo(tc.timer) -} - -func (tc *testConn) timerDelay() time.Duration { - if tc.timer.IsZero() { - return math.MaxInt64 // infinite - } - if tc.timer.Before(tc.endpoint.now) { - return 0 - } - return tc.timer.Sub(tc.endpoint.now) + time.Sleep(time.Until(when)) + synctest.Wait() } const infiniteDuration = time.Duration(math.MaxInt64) // timeUntilEvent returns the amount of time until the next connection event. func (tc *testConn) timeUntilEvent() time.Duration { - if tc.timer.IsZero() { + next := tc.nextEvent() + if next.IsZero() { return infiniteDuration } - if tc.timer.Before(tc.endpoint.now) { - return 0 - } - return tc.timer.Sub(tc.endpoint.now) + return max(0, time.Until(next)) } -// wait blocks until the conn becomes idle. -// The conn is idle when it is blocked waiting for a packet to arrive or a timer to expire. -// Tests shouldn't need to call wait directly. -// testConn methods that wake the Conn event loop will call wait for them. -func (tc *testConn) wait() { - tc.t.Helper() - idlec := make(chan struct{}) - fail := false - tc.conn.sendMsg(func(now time.Time, c *Conn) { - if tc.idlec != nil { - tc.t.Errorf("testConn.wait called concurrently") - fail = true - close(idlec) - } else { - // nextMessage will close idlec. - tc.idlec = idlec - } +func (tc *testConn) nextEvent() time.Time { + nextc := make(chan time.Time) + tc.conn.sendMsg(func(now, next time.Time, c *Conn) { + nextc <- next }) - select { - case <-idlec: - case <-tc.conn.donec: - // We may have async ops that can proceed now that the conn is done. - tc.wakeAsync() - } - if fail { - panic(fail) - } + return <-nextc } func (tc *testConn) cleanup() { @@ -498,7 +450,7 @@ func (tc *testConn) ignoreFrame(frameType byte) { // It returns nil if the Conn has no more datagrams to send at this time. func (tc *testConn) readDatagram() *testDatagram { tc.t.Helper() - tc.wait() + synctest.Wait() tc.sentPackets = nil tc.sentFrames = nil buf := tc.endpoint.read() @@ -1103,48 +1055,10 @@ func (tc *testConnHooks) handleTLSEvent(e tls.QUICEvent) { } } -// nextMessage is called by the Conn's event loop to request its next event. -func (tc *testConnHooks) nextMessage(msgc chan any, timer time.Time) (now time.Time, m any) { - tc.timer = timer - for { - if !timer.IsZero() && !timer.After(tc.endpoint.now) { - if timer.Equal(tc.timerLastFired) { - // If the connection timer fires at time T, the Conn should take some - // action to advance the timer into the future. If the Conn reschedules - // the timer for the same time, it isn't making progress and we have a bug. - tc.t.Errorf("connection timer spinning; now=%v timer=%v", tc.endpoint.now, timer) - } else { - tc.timerLastFired = timer - return tc.endpoint.now, timerEvent{} - } - } - select { - case m := <-msgc: - return tc.endpoint.now, m - default: - } - if !tc.wakeAsync() { - break - } - } - // If the message queue is empty, then the conn is idle. - if tc.idlec != nil { - idlec := tc.idlec - tc.idlec = nil - close(idlec) - } - m = <-msgc - return tc.endpoint.now, m -} - func (tc *testConnHooks) newConnID(seq int64) ([]byte, error) { return testLocalConnID(seq), nil } -func (tc *testConnHooks) timeNow() time.Time { - return tc.endpoint.now -} - // testLocalConnID returns the connection ID with a given sequence number // used by a Conn under test. func testLocalConnID(seq int64) []byte { diff --git a/quic/endpoint.go b/quic/endpoint.go index 1bb90152..3d68073c 100644 --- a/quic/endpoint.go +++ b/quic/endpoint.go @@ -36,7 +36,6 @@ type Endpoint struct { } type endpointTestHooks interface { - timeNow() time.Time newConn(c *Conn) } @@ -160,7 +159,7 @@ func (e *Endpoint) Close(ctx context.Context) error { // Accept waits for and returns the next connection. func (e *Endpoint) Accept(ctx context.Context) (*Conn, error) { - return e.acceptQueue.get(ctx, nil) + return e.acceptQueue.get(ctx) } // Dial creates and returns a connection to a network address. @@ -269,12 +268,7 @@ func (e *Endpoint) handleUnknownDestinationDatagram(m *datagram) { if len(m.b) < minimumValidPacketSize { return } - var now time.Time - if e.testHooks != nil { - now = e.testHooks.timeNow() - } else { - now = time.Now() - } + now := time.Now() // Check to see if this is a stateless reset. var token statelessResetToken copy(token[:], m.b[len(m.b)-len(token):]) diff --git a/quic/endpoint_test.go b/quic/endpoint_test.go index 7ec81395..6a62104e 100644 --- a/quic/endpoint_test.go +++ b/quic/endpoint_test.go @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +//go:build go1.25 + package quic import ( @@ -12,8 +14,9 @@ import ( "log/slog" "net/netip" "runtime" + "sync" "testing" - "time" + "testing/synctest" "golang.org/x/net/quic/qlog" ) @@ -126,22 +129,22 @@ func makeTestConfig(conf *Config, side connSide) *Config { type testEndpoint struct { t *testing.T e *Endpoint - now time.Time recvc chan *datagram idlec chan struct{} conns map[*Conn]*testConn acceptQueue []*testConn configTransportParams []func(*transportParameters) configTestConn []func(*testConn) - sentDatagrams [][]byte peerTLSConn *tls.QUICConn lastInitialDstConnID []byte // for parsing Retry packets + + sentDatagramsMu sync.Mutex + sentDatagrams [][]byte } func newTestEndpoint(t *testing.T, config *Config) *testEndpoint { te := &testEndpoint{ t: t, - now: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), recvc: make(chan *datagram), idlec: make(chan struct{}), conns: make(map[*Conn]*testConn), @@ -159,16 +162,6 @@ func (te *testEndpoint) cleanup() { te.e.Close(canceledContext()) } -func (te *testEndpoint) wait() { - select { - case te.idlec <- struct{}{}: - case <-te.e.closec: - } - for _, tc := range te.conns { - tc.wait() - } -} - // accept returns a server connection from the endpoint. // Unlike Endpoint.Accept, connections are available as soon as they are created. func (te *testEndpoint) accept() *testConn { @@ -182,7 +175,7 @@ func (te *testEndpoint) accept() *testConn { func (te *testEndpoint) write(d *datagram) { te.recvc <- d - te.wait() + synctest.Wait() } var testClientAddr = netip.MustParseAddrPort("10.0.0.1:8000") @@ -241,7 +234,9 @@ func (te *testEndpoint) connForSource(srcConnID []byte) *testConn { func (te *testEndpoint) read() []byte { te.t.Helper() - te.wait() + synctest.Wait() + te.sentDatagramsMu.Lock() + defer te.sentDatagramsMu.Unlock() if len(te.sentDatagrams) == 0 { return nil } @@ -279,34 +274,9 @@ func (te *testEndpoint) wantIdle(expectation string) { } } -// advance causes time to pass. -func (te *testEndpoint) advance(d time.Duration) { - te.t.Helper() - te.advanceTo(te.now.Add(d)) -} - -// advanceTo sets the current time. -func (te *testEndpoint) advanceTo(now time.Time) { - te.t.Helper() - if te.now.After(now) { - te.t.Fatalf("time moved backwards: %v -> %v", te.now, now) - } - te.now = now - for _, tc := range te.conns { - if !tc.timer.After(te.now) { - tc.conn.sendMsg(timerEvent{}) - tc.wait() - } - } -} - // testEndpointHooks implements endpointTestHooks. type testEndpointHooks testEndpoint -func (te *testEndpointHooks) timeNow() time.Time { - return te.now -} - func (te *testEndpointHooks) newConn(c *Conn) { tc := newTestConnForConn(te.t, (*testEndpoint)(te), c) te.conns[c] = tc @@ -338,6 +308,8 @@ func (te *testEndpointUDPConn) Read(f func(*datagram)) { } func (te *testEndpointUDPConn) Write(dgram datagram) error { + te.sentDatagramsMu.Lock() + defer te.sentDatagramsMu.Unlock() te.sentDatagrams = append(te.sentDatagrams, append([]byte(nil), dgram.b...)) return nil } diff --git a/quic/gate.go b/quic/gate.go index 1f570bb9..b8b8605e 100644 --- a/quic/gate.go +++ b/quic/gate.go @@ -46,10 +46,7 @@ func (g *gate) lock() (set bool) { // waitAndLock waits until the condition is set before acquiring the gate. // If the context expires, waitAndLock returns an error and does not acquire the gate. -func (g *gate) waitAndLock(ctx context.Context, testHooks connTestHooks) error { - if testHooks != nil { - return testHooks.waitUntil(ctx, g.lockIfSet) - } +func (g *gate) waitAndLock(ctx context.Context) error { select { case <-g.set: return nil diff --git a/quic/gate_test.go b/quic/gate_test.go index 54f7a8a4..59c157d2 100644 --- a/quic/gate_test.go +++ b/quic/gate_test.go @@ -47,7 +47,7 @@ func TestGateWaitAndLockContext(t *testing.T) { time.Sleep(1 * time.Millisecond) cancel() }() - if err := g.waitAndLock(ctx, nil); err != context.Canceled { + if err := g.waitAndLock(ctx); err != context.Canceled { t.Errorf("g.waitAndLock() = %v, want context.Canceled", err) } // waitAndLock succeeds @@ -58,7 +58,7 @@ func TestGateWaitAndLockContext(t *testing.T) { set = true g.unlock(true) }() - if err := g.waitAndLock(context.Background(), nil); err != nil { + if err := g.waitAndLock(context.Background()); err != nil { t.Errorf("g.waitAndLock() = %v, want nil", err) } if !set { @@ -66,7 +66,7 @@ func TestGateWaitAndLockContext(t *testing.T) { } g.unlock(true) // waitAndLock succeeds when the gate is set and the context is canceled - if err := g.waitAndLock(ctx, nil); err != nil { + if err := g.waitAndLock(ctx); err != nil { t.Errorf("g.waitAndLock() = %v, want nil", err) } } @@ -89,5 +89,5 @@ func TestGateUnlockFunc(t *testing.T) { g.lock() defer g.unlockFunc(func() bool { return true }) }() - g.waitAndLock(context.Background(), nil) + g.waitAndLock(context.Background()) } diff --git a/quic/idle_test.go b/quic/idle_test.go index 29d3bd14..d9ae16ab 100644 --- a/quic/idle_test.go +++ b/quic/idle_test.go @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +//go:build go1.25 + package quic import ( @@ -9,10 +11,14 @@ import ( "crypto/tls" "fmt" "testing" + "testing/synctest" "time" ) func TestHandshakeTimeoutExpiresServer(t *testing.T) { + synctest.Test(t, testHandshakeTimeoutExpiresServer) +} +func testHandshakeTimeoutExpiresServer(t *testing.T) { const timeout = 5 * time.Second tc := newTestConn(t, serverSide, func(c *Config) { c.HandshakeTimeout = timeout @@ -32,18 +38,18 @@ func TestHandshakeTimeoutExpiresServer(t *testing.T) { packetTypeHandshake, debugFrameCrypto{}) tc.writeAckForAll() - if got, want := tc.timerDelay(), timeout; got != want { + if got, want := tc.timeUntilEvent(), timeout; got != want { t.Errorf("connection timer = %v, want %v (handshake timeout)", got, want) } // Client sends a packet, but this does not extend the handshake timer. - tc.advance(1 * time.Second) + time.Sleep(1 * time.Second) tc.writeFrames(packetTypeHandshake, debugFrameCrypto{ data: tc.cryptoDataIn[tls.QUICEncryptionLevelHandshake][:1], // partial data }) tc.wantIdle("handshake is not complete") - tc.advance(timeout - 1*time.Second) + time.Sleep(timeout - 1*time.Second) tc.wantFrame("server closes connection after handshake timeout", packetTypeHandshake, debugFrameConnectionCloseTransport{ code: errConnectionRefused, @@ -51,6 +57,9 @@ func TestHandshakeTimeoutExpiresServer(t *testing.T) { } func TestHandshakeTimeoutExpiresClient(t *testing.T) { + synctest.Test(t, testHandshakeTimeoutExpiresClient) +} +func testHandshakeTimeoutExpiresClient(t *testing.T) { const timeout = 5 * time.Second tc := newTestConn(t, clientSide, func(c *Config) { c.HandshakeTimeout = timeout @@ -77,10 +86,10 @@ func TestHandshakeTimeoutExpiresClient(t *testing.T) { tc.writeAckForAll() tc.wantIdle("client is waiting for end of handshake") - if got, want := tc.timerDelay(), timeout; got != want { + if got, want := tc.timeUntilEvent(), timeout; got != want { t.Errorf("connection timer = %v, want %v (handshake timeout)", got, want) } - tc.advance(timeout) + time.Sleep(timeout) tc.wantFrame("client closes connection after handshake timeout", packetTypeHandshake, debugFrameConnectionCloseTransport{ code: errConnectionRefused, @@ -110,7 +119,7 @@ func TestIdleTimeoutExpires(t *testing.T) { wantTimeout: 10 * time.Second, }} { name := fmt.Sprintf("local=%v/peer=%v", test.localMaxIdleTimeout, test.peerMaxIdleTimeout) - t.Run(name, func(t *testing.T) { + synctestSubtest(t, name, func(t *testing.T) { tc := newTestConn(t, serverSide, func(p *transportParameters) { p.maxIdleTimeout = test.peerMaxIdleTimeout }, func(c *Config) { @@ -120,13 +129,13 @@ func TestIdleTimeoutExpires(t *testing.T) { if got, want := tc.timeUntilEvent(), test.wantTimeout; got != want { t.Errorf("new conn timeout=%v, want %v (idle timeout)", got, want) } - tc.advance(test.wantTimeout - 1) + time.Sleep(test.wantTimeout - 1) tc.wantIdle("connection is idle and alive prior to timeout") ctx := canceledContext() if err := tc.conn.Wait(ctx); err != context.Canceled { t.Fatalf("conn.Wait() = %v, want Canceled", err) } - tc.advance(1) + time.Sleep(1) tc.wantIdle("connection exits after timeout") if err := tc.conn.Wait(ctx); err != errIdleTimeout { t.Fatalf("conn.Wait() = %v, want errIdleTimeout", err) @@ -154,7 +163,7 @@ func TestIdleTimeoutKeepAlive(t *testing.T) { wantTimeout: 30 * time.Second, }} { name := fmt.Sprintf("idle_timeout=%v/keepalive=%v", test.idleTimeout, test.keepAlive) - t.Run(name, func(t *testing.T) { + synctestSubtest(t, name, func(t *testing.T) { tc := newTestConn(t, serverSide, func(c *Config) { c.MaxIdleTimeout = test.idleTimeout c.KeepAlivePeriod = test.keepAlive @@ -163,9 +172,9 @@ func TestIdleTimeoutKeepAlive(t *testing.T) { if got, want := tc.timeUntilEvent(), test.wantTimeout; got != want { t.Errorf("new conn timeout=%v, want %v (keepalive timeout)", got, want) } - tc.advance(test.wantTimeout - 1) + time.Sleep(test.wantTimeout - 1) tc.wantIdle("connection is idle prior to timeout") - tc.advance(1) + time.Sleep(1) tc.wantFrameType("keep-alive ping is sent", packetType1RTT, debugFramePing{}) }) @@ -173,6 +182,9 @@ func TestIdleTimeoutKeepAlive(t *testing.T) { } func TestIdleLongTermKeepAliveSent(t *testing.T) { + synctest.Test(t, testIdleLongTermKeepAliveSent) +} +func testIdleLongTermKeepAliveSent(t *testing.T) { // This test examines a connection sitting idle and sending periodic keep-alive pings. const keepAlivePeriod = 30 * time.Second tc := newTestConn(t, clientSide, func(c *Config) { @@ -191,7 +203,7 @@ func TestIdleLongTermKeepAliveSent(t *testing.T) { if got, want := tc.timeUntilEvent(), keepAlivePeriod; got != want { t.Errorf("i=%v conn timeout=%v, want %v (keepalive timeout)", i, got, want) } - tc.advance(keepAlivePeriod) + time.Sleep(keepAlivePeriod) tc.wantFrameType("keep-alive ping is sent", packetType1RTT, debugFramePing{}) tc.writeAckForAll() @@ -199,6 +211,9 @@ func TestIdleLongTermKeepAliveSent(t *testing.T) { } func TestIdleLongTermKeepAliveReceived(t *testing.T) { + synctest.Test(t, testIdleLongTermKeepAliveReceived) +} +func testIdleLongTermKeepAliveReceived(t *testing.T) { // This test examines a connection sitting idle, but receiving periodic peer // traffic to keep the connection alive. const idleTimeout = 30 * time.Second @@ -207,7 +222,7 @@ func TestIdleLongTermKeepAliveReceived(t *testing.T) { }) tc.handshake() for i := 0; i < 10; i++ { - tc.advance(idleTimeout - 1*time.Second) + time.Sleep(idleTimeout - 1*time.Second) tc.writeFrames(packetType1RTT, debugFramePing{}) if got, want := tc.timeUntilEvent(), maxAckDelay-timerGranularity; got != want { t.Errorf("i=%v conn timeout=%v, want %v (max_ack_delay)", i, got, want) diff --git a/quic/key_update_test.go b/quic/key_update_test.go index 2daf7db9..7a02e849 100644 --- a/quic/key_update_test.go +++ b/quic/key_update_test.go @@ -2,13 +2,19 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +//go:build go1.25 + package quic import ( "testing" + "testing/synctest" ) func TestKeyUpdatePeerUpdates(t *testing.T) { + synctest.Test(t, testKeyUpdatePeerUpdates) +} +func testKeyUpdatePeerUpdates(t *testing.T) { tc := newTestConn(t, serverSide) tc.handshake() tc.ignoreFrames = nil // ignore nothing @@ -56,6 +62,9 @@ func TestKeyUpdatePeerUpdates(t *testing.T) { } func TestKeyUpdateAcceptPreviousPhaseKeys(t *testing.T) { + synctest.Test(t, testKeyUpdateAcceptPreviousPhaseKeys) +} +func testKeyUpdateAcceptPreviousPhaseKeys(t *testing.T) { // "An endpoint SHOULD retain old keys for some time after // unprotecting a packet sent using the new keys." // https://www.rfc-editor.org/rfc/rfc9001#section-6.1-8 @@ -112,6 +121,9 @@ func TestKeyUpdateAcceptPreviousPhaseKeys(t *testing.T) { } func TestKeyUpdateRejectPacketFromPriorPhase(t *testing.T) { + synctest.Test(t, testKeyUpdateRejectPacketFromPriorPhase) +} +func testKeyUpdateRejectPacketFromPriorPhase(t *testing.T) { // "Packets with higher packet numbers MUST be protected with either // the same or newer packet protection keys than packets with lower packet numbers." // https://www.rfc-editor.org/rfc/rfc9001#section-6.4-2 @@ -161,6 +173,9 @@ func TestKeyUpdateRejectPacketFromPriorPhase(t *testing.T) { } func TestKeyUpdateLocallyInitiated(t *testing.T) { + synctest.Test(t, testKeyUpdateLocallyInitiated) +} +func testKeyUpdateLocallyInitiated(t *testing.T) { const updateAfter = 4 // initiate key update after 1-RTT packet 4 tc := newTestConn(t, serverSide) tc.conn.keysAppData.updateAfter = updateAfter diff --git a/quic/packet_codec_test.go b/quic/packet_codec_test.go index 4ae22b38..d49f0ea6 100644 --- a/quic/packet_codec_test.go +++ b/quic/packet_codec_test.go @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +//go:build go1.25 + package quic import ( diff --git a/quic/path_test.go b/quic/path_test.go index 60ff51e3..16dd9fce 100644 --- a/quic/path_test.go +++ b/quic/path_test.go @@ -2,10 +2,13 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +//go:build go1.25 + package quic import ( "testing" + "testing/synctest" ) func TestPathChallengeReceived(t *testing.T) { @@ -22,30 +25,35 @@ func TestPathChallengeReceived(t *testing.T) { padTo: 1200, wantPadding: 1200, }} { - // "The recipient of [a PATH_CHALLENGE] frame MUST generate - // a PATH_RESPONSE frame [...] containing the same Data value." - // https://www.rfc-editor.org/rfc/rfc9000.html#section-19.17-7 - tc := newTestConn(t, clientSide) - tc.handshake() - tc.ignoreFrame(frameTypeAck) - data := pathChallengeData{0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef} - tc.writeFrames(packetType1RTT, debugFramePathChallenge{ - data: data, - }, debugFramePadding{ - to: test.padTo, - }) - tc.wantFrame("response to PATH_CHALLENGE", - packetType1RTT, debugFramePathResponse{ + synctestSubtest(t, test.name, func(t *testing.T) { + // "The recipient of [a PATH_CHALLENGE] frame MUST generate + // a PATH_RESPONSE frame [...] containing the same Data value." + // https://www.rfc-editor.org/rfc/rfc9000.html#section-19.17-7 + tc := newTestConn(t, clientSide) + tc.handshake() + tc.ignoreFrame(frameTypeAck) + data := pathChallengeData{0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef} + tc.writeFrames(packetType1RTT, debugFramePathChallenge{ data: data, + }, debugFramePadding{ + to: test.padTo, }) - if got, want := tc.lastDatagram.paddedSize, test.wantPadding; got != want { - t.Errorf("PATH_RESPONSE expanded to %v bytes, want %v", got, want) - } - tc.wantIdle("connection is idle") + tc.wantFrame("response to PATH_CHALLENGE", + packetType1RTT, debugFramePathResponse{ + data: data, + }) + if got, want := tc.lastDatagram.paddedSize, test.wantPadding; got != want { + t.Errorf("PATH_RESPONSE expanded to %v bytes, want %v", got, want) + } + tc.wantIdle("connection is idle") + }) } } func TestPathResponseMismatchReceived(t *testing.T) { + synctest.Test(t, testPathResponseMismatchReceived) +} +func testPathResponseMismatchReceived(t *testing.T) { // "If the content of a PATH_RESPONSE frame does not match the content of // a PATH_CHALLENGE frame previously sent by the endpoint, // the endpoint MAY generate a connection error of type PROTOCOL_VIOLATION." diff --git a/quic/ping_test.go b/quic/ping_test.go index a8e6b61a..4589a6c7 100644 --- a/quic/ping_test.go +++ b/quic/ping_test.go @@ -2,11 +2,19 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +//go:build go1.25 + package quic -import "testing" +import ( + "testing" + "testing/synctest" +) func TestPing(t *testing.T) { + synctest.Test(t, testPing) +} +func testPing(t *testing.T) { tc := newTestConn(t, clientSide) tc.handshake() @@ -22,6 +30,9 @@ func TestPing(t *testing.T) { } func TestAck(t *testing.T) { + synctest.Test(t, testAck) +} +func testAck(t *testing.T) { tc := newTestConn(t, serverSide) tc.handshake() diff --git a/quic/qlog_test.go b/quic/qlog_test.go index 08c2a77a..47e46711 100644 --- a/quic/qlog_test.go +++ b/quic/qlog_test.go @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +//go:build go1.25 + package quic import ( @@ -12,14 +14,16 @@ import ( "io" "log/slog" "reflect" + "sync" "testing" + "testing/synctest" "time" "golang.org/x/net/quic/qlog" ) func TestQLogHandshake(t *testing.T) { - testSides(t, "", func(t *testing.T, side connSide) { + testSidesSynctest(t, "", func(t *testing.T, side connSide) { qr := &qlogRecord{} tc := newTestConn(t, side, qr.config) tc.handshake() @@ -55,6 +59,9 @@ func TestQLogHandshake(t *testing.T) { } func TestQLogPacketFrames(t *testing.T) { + synctest.Test(t, testQLogPacketFrames) +} +func testQLogPacketFrames(t *testing.T) { qr := &qlogRecord{} tc := newTestConn(t, clientSide, qr.config) tc.handshake() @@ -111,7 +118,7 @@ func TestQLogConnectionClosedTrigger(t *testing.T) { tc.ignoreFrame(frameTypeCrypto) tc.ignoreFrame(frameTypeAck) tc.ignoreFrame(frameTypePing) - tc.advance(5 * time.Second) + time.Sleep(5 * time.Second) }, }, { trigger: "idle_timeout", @@ -122,7 +129,7 @@ func TestQLogConnectionClosedTrigger(t *testing.T) { }, f: func(tc *testConn) { tc.handshake() - tc.advance(5 * time.Second) + time.Sleep(5 * time.Second) }, }, { trigger: "error", @@ -134,7 +141,7 @@ func TestQLogConnectionClosedTrigger(t *testing.T) { tc.conn.Abort(nil) }, }} { - t.Run(test.trigger, func(t *testing.T) { + synctestSubtest(t, test.trigger, func(t *testing.T) { qr := &qlogRecord{} tc := newTestConn(t, clientSide, append(test.connOpts, qr.config)...) test.f(tc) @@ -147,7 +154,7 @@ func TestQLogConnectionClosedTrigger(t *testing.T) { t.Fatalf("unexpected frame: %v", fr) } tc.wantIdle("connection should be idle while closing") - tc.advance(5 * time.Second) // long enough for the drain timer to expire + time.Sleep(5 * time.Second) // long enough for the drain timer to expire qr.wantEvents(t, jsonEvent{ "name": "connectivity:connection_closed", "data": map[string]any{ @@ -159,6 +166,9 @@ func TestQLogConnectionClosedTrigger(t *testing.T) { } func TestQLogRecovery(t *testing.T) { + synctest.Test(t, testQLogRecovery) +} +func testQLogRecovery(t *testing.T) { qr := &qlogRecord{} tc, s := newTestConnAndLocalStream(t, clientSide, uniStream, permissiveTransportParameters, qr.config) @@ -198,6 +208,9 @@ func TestQLogRecovery(t *testing.T) { } func TestQLogLoss(t *testing.T) { + synctest.Test(t, testQLogLoss) +} +func testQLogLoss(t *testing.T) { qr := &qlogRecord{} tc, s := newTestConnAndLocalStream(t, clientSide, uniStream, permissiveTransportParameters, qr.config) @@ -230,6 +243,9 @@ func TestQLogLoss(t *testing.T) { } func TestQLogPacketDropped(t *testing.T) { + synctest.Test(t, testQLogPacketDropped) +} +func testQLogPacketDropped(t *testing.T) { qr := &qlogRecord{} tc := newTestConn(t, clientSide, permissiveTransportParameters, qr.config) tc.handshake() @@ -324,10 +340,13 @@ func jsonPartialEqual(got, want any) (equal bool) { // A qlogRecord records events. type qlogRecord struct { + mu sync.Mutex ev []jsonEvent } func (q *qlogRecord) Write(b []byte) (int, error) { + q.mu.Lock() + defer q.mu.Unlock() // This relies on the property that the Handler always makes one Write call per event. if len(b) < 1 || b[0] != 0x1e { panic(fmt.Errorf("trace Write should start with record separator, got %q", string(b))) @@ -355,6 +374,8 @@ func (q *qlogRecord) config(c *Config) { // wantEvents checks that every event in want occurs in the order specified. func (q *qlogRecord) wantEvents(t *testing.T, want ...jsonEvent) { t.Helper() + q.mu.Lock() + defer q.mu.Unlock() got := q.ev if !jsonPartialEqual(got, want) { t.Fatalf("got events:\n%v\n\nwant events:\n%v", got, want) diff --git a/quic/queue.go b/quic/queue.go index 8b90ae77..f2712f40 100644 --- a/quic/queue.go +++ b/quic/queue.go @@ -42,9 +42,9 @@ func (q *queue[T]) put(v T) bool { // get removes the first item from the queue, blocking until ctx is done, an item is available, // or the queue is closed. -func (q *queue[T]) get(ctx context.Context, testHooks connTestHooks) (T, error) { +func (q *queue[T]) get(ctx context.Context) (T, error) { var zero T - if err := q.gate.waitAndLock(ctx, testHooks); err != nil { + if err := q.gate.waitAndLock(ctx); err != nil { return zero, err } defer q.unlock() diff --git a/quic/queue_test.go b/quic/queue_test.go index b5835214..a3907f31 100644 --- a/quic/queue_test.go +++ b/quic/queue_test.go @@ -16,7 +16,7 @@ func TestQueue(t *testing.T) { cancel() q := newQueue[int]() - if got, err := q.get(nonblocking, nil); err != context.Canceled { + if got, err := q.get(nonblocking); err != context.Canceled { t.Fatalf("q.get() = %v, %v, want nil, context.Canceled", got, err) } @@ -26,13 +26,13 @@ func TestQueue(t *testing.T) { if !q.put(2) { t.Fatalf("q.put(2) = false, want true") } - if got, err := q.get(nonblocking, nil); got != 1 || err != nil { + if got, err := q.get(nonblocking); got != 1 || err != nil { t.Fatalf("q.get() = %v, %v, want 1, nil", got, err) } - if got, err := q.get(nonblocking, nil); got != 2 || err != nil { + if got, err := q.get(nonblocking); got != 2 || err != nil { t.Fatalf("q.get() = %v, %v, want 2, nil", got, err) } - if got, err := q.get(nonblocking, nil); err != context.Canceled { + if got, err := q.get(nonblocking); err != context.Canceled { t.Fatalf("q.get() = %v, %v, want nil, context.Canceled", got, err) } @@ -40,7 +40,7 @@ func TestQueue(t *testing.T) { time.Sleep(1 * time.Millisecond) q.put(3) }() - if got, err := q.get(context.Background(), nil); got != 3 || err != nil { + if got, err := q.get(context.Background()); got != 3 || err != nil { t.Fatalf("q.get() = %v, %v, want 3, nil", got, err) } @@ -48,7 +48,7 @@ func TestQueue(t *testing.T) { t.Fatalf("q.put(2) = false, want true") } q.close(io.EOF) - if got, err := q.get(context.Background(), nil); got != 0 || err != io.EOF { + if got, err := q.get(context.Background()); got != 0 || err != io.EOF { t.Fatalf("q.get() = %v, %v, want 0, io.EOF", got, err) } if q.put(5) { diff --git a/quic/quic_test.go b/quic/quic_test.go index 071003e9..cdcc0d78 100644 --- a/quic/quic_test.go +++ b/quic/quic_test.go @@ -2,10 +2,13 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +//go:build go1.25 + package quic import ( "testing" + "testing/synctest" ) func testSides(t *testing.T, name string, f func(*testing.T, connSide)) { @@ -16,6 +19,16 @@ func testSides(t *testing.T, name string, f func(*testing.T, connSide)) { t.Run(name+"client", func(t *testing.T) { f(t, clientSide) }) } +func testSidesSynctest(t *testing.T, name string, f func(*testing.T, connSide)) { + t.Helper() + testSides(t, name, func(t *testing.T, side connSide) { + t.Helper() + synctest.Test(t, func(t *testing.T) { + f(t, side) + }) + }) +} + func testStreamTypes(t *testing.T, name string, f func(*testing.T, streamType)) { if name != "" { name += "/" @@ -24,6 +37,16 @@ func testStreamTypes(t *testing.T, name string, f func(*testing.T, streamType)) t.Run(name+"uni", func(t *testing.T) { f(t, uniStream) }) } +func testStreamTypesSynctest(t *testing.T, name string, f func(*testing.T, streamType)) { + t.Helper() + testStreamTypes(t, name, func(t *testing.T, stype streamType) { + t.Helper() + synctest.Test(t, func(t *testing.T) { + f(t, stype) + }) + }) +} + func testSidesAndStreamTypes(t *testing.T, name string, f func(*testing.T, connSide, streamType)) { if name != "" { name += "/" @@ -33,3 +56,20 @@ func testSidesAndStreamTypes(t *testing.T, name string, f func(*testing.T, connS t.Run(name+"server/uni", func(t *testing.T) { f(t, serverSide, uniStream) }) t.Run(name+"client/uni", func(t *testing.T) { f(t, clientSide, uniStream) }) } + +func testSidesAndStreamTypesSynctest(t *testing.T, name string, f func(*testing.T, connSide, streamType)) { + t.Helper() + testSidesAndStreamTypes(t, name, func(t *testing.T, side connSide, stype streamType) { + t.Helper() + synctest.Test(t, func(t *testing.T) { + f(t, side, stype) + }) + }) +} + +func synctestSubtest(t *testing.T, name string, f func(t *testing.T)) { + t.Run(name, func(t *testing.T) { + t.Helper() + synctest.Test(t, f) + }) +} diff --git a/quic/retry_test.go b/quic/retry_test.go index d6f02547..7a4481c0 100644 --- a/quic/retry_test.go +++ b/quic/retry_test.go @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +//go:build go1.25 + package quic import ( @@ -10,6 +12,7 @@ import ( "crypto/tls" "net/netip" "testing" + "testing/synctest" "time" ) @@ -77,9 +80,12 @@ func newRetryServerTest(t *testing.T) *retryServerTest { } func TestRetryServerSucceeds(t *testing.T) { + synctest.Test(t, testRetryServerSucceeds) +} +func testRetryServerSucceeds(t *testing.T) { rt := newRetryServerTest(t) te := rt.te - te.advance(retryTokenValidityPeriod) + time.Sleep(retryTokenValidityPeriod) te.writeDatagram(&testDatagram{ packets: []*testPacket{{ ptype: packetTypeInitial, @@ -117,6 +123,9 @@ func TestRetryServerSucceeds(t *testing.T) { } func TestRetryServerTokenInvalid(t *testing.T) { + synctest.Test(t, testRetryServerTokenInvalid) +} +func testRetryServerTokenInvalid(t *testing.T) { // "If a server receives a client Initial that contains an invalid Retry token [...] // the server SHOULD immediately close [...] the connection with an // INVALID_TOKEN error." @@ -147,11 +156,14 @@ func TestRetryServerTokenInvalid(t *testing.T) { } func TestRetryServerTokenTooOld(t *testing.T) { + synctest.Test(t, testRetryServerTokenTooOld) +} +func testRetryServerTokenTooOld(t *testing.T) { // "[...] a token SHOULD have an expiration time [...]" // https://www.rfc-editor.org/rfc/rfc9000#section-8.1.3-3 rt := newRetryServerTest(t) te := rt.te - te.advance(retryTokenValidityPeriod + time.Second) + time.Sleep(retryTokenValidityPeriod + time.Second) te.writeDatagram(&testDatagram{ packets: []*testPacket{{ ptype: packetTypeInitial, @@ -176,6 +188,9 @@ func TestRetryServerTokenTooOld(t *testing.T) { } func TestRetryServerTokenWrongIP(t *testing.T) { + synctest.Test(t, testRetryServerTokenWrongIP) +} +func testRetryServerTokenWrongIP(t *testing.T) { // "Tokens sent in Retry packets SHOULD include information that allows the server // to verify that the source IP address and port in client packets remain constant." // https://www.rfc-editor.org/rfc/rfc9000#section-8.1.4-3 @@ -206,6 +221,9 @@ func TestRetryServerTokenWrongIP(t *testing.T) { } func TestRetryServerIgnoresRetry(t *testing.T) { + synctest.Test(t, testRetryServerIgnoresRetry) +} +func testRetryServerIgnoresRetry(t *testing.T) { tc := newTestConn(t, serverSide) tc.handshake() tc.write(&testDatagram{ @@ -225,6 +243,9 @@ func TestRetryServerIgnoresRetry(t *testing.T) { } func TestRetryClientSuccess(t *testing.T) { + synctest.Test(t, testRetryClientSuccess) +} +func testRetryClientSuccess(t *testing.T) { // "This token MUST be repeated by the client in all Initial packets it sends // for that connection after it receives the Retry packet." // https://www.rfc-editor.org/rfc/rfc9000#section-8.1.2-1 @@ -323,7 +344,7 @@ func TestRetryClientInvalidServerTransportParameters(t *testing.T) { p.retrySrcConnID = []byte("invalid") }, }} { - t.Run(test.name, func(t *testing.T) { + synctestSubtest(t, test.name, func(t *testing.T) { tc := newTestConn(t, clientSide, func(p *transportParameters) { p.initialSrcConnID = initialSrcConnID @@ -367,6 +388,9 @@ func TestRetryClientInvalidServerTransportParameters(t *testing.T) { } func TestRetryClientIgnoresRetryAfterReceivingPacket(t *testing.T) { + synctest.Test(t, testRetryClientIgnoresRetryAfterReceivingPacket) +} +func testRetryClientIgnoresRetryAfterReceivingPacket(t *testing.T) { // "After the client has received and processed an Initial or Retry packet // from the server, it MUST discard any subsequent Retry packets that it receives." // https://www.rfc-editor.org/rfc/rfc9000#section-17.2.5.2-1 @@ -401,6 +425,9 @@ func TestRetryClientIgnoresRetryAfterReceivingPacket(t *testing.T) { } func TestRetryClientIgnoresRetryAfterReceivingRetry(t *testing.T) { + synctest.Test(t, testRetryClientIgnoresRetryAfterReceivingRetry) +} +func testRetryClientIgnoresRetryAfterReceivingRetry(t *testing.T) { // "After the client has received and processed an Initial or Retry packet // from the server, it MUST discard any subsequent Retry packets that it receives." // https://www.rfc-editor.org/rfc/rfc9000#section-17.2.5.2-1 @@ -424,6 +451,9 @@ func TestRetryClientIgnoresRetryAfterReceivingRetry(t *testing.T) { } func TestRetryClientIgnoresRetryWithInvalidIntegrityTag(t *testing.T) { + synctest.Test(t, testRetryClientIgnoresRetryWithInvalidIntegrityTag) +} +func testRetryClientIgnoresRetryWithInvalidIntegrityTag(t *testing.T) { tc := newTestConn(t, clientSide) tc.wantFrameType("client Initial CRYPTO data", packetTypeInitial, debugFrameCrypto{}) @@ -441,6 +471,9 @@ func TestRetryClientIgnoresRetryWithInvalidIntegrityTag(t *testing.T) { } func TestRetryClientIgnoresRetryWithZeroLengthToken(t *testing.T) { + synctest.Test(t, testRetryClientIgnoresRetryWithZeroLengthToken) +} +func testRetryClientIgnoresRetryWithZeroLengthToken(t *testing.T) { // "A client MUST discard a Retry packet with a zero-length Retry Token field." // https://www.rfc-editor.org/rfc/rfc9000#section-17.2.5.2-2 tc := newTestConn(t, clientSide) diff --git a/quic/skip_test.go b/quic/skip_test.go index 1fcb735f..2c33378b 100644 --- a/quic/skip_test.go +++ b/quic/skip_test.go @@ -2,11 +2,19 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +//go:build go1.25 + package quic -import "testing" +import ( + "testing" + "testing/synctest" +) func TestSkipPackets(t *testing.T) { + synctest.Test(t, testSkipPackets) +} +func testSkipPackets(t *testing.T) { tc, s := newTestConnAndLocalStream(t, serverSide, uniStream, permissiveTransportParameters) connWritesPacket := func() { s.WriteByte(0) @@ -39,6 +47,9 @@ expectSkip: } func TestSkipAckForSkippedPacket(t *testing.T) { + synctest.Test(t, testSkipAckForSkippedPacket) +} +func testSkipAckForSkippedPacket(t *testing.T) { tc, s := newTestConnAndLocalStream(t, serverSide, uniStream, permissiveTransportParameters) // Cause the connection to send packets until it skips a packet number. diff --git a/quic/stateless_reset_test.go b/quic/stateless_reset_test.go index 33d467a9..94737508 100644 --- a/quic/stateless_reset_test.go +++ b/quic/stateless_reset_test.go @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +//go:build go1.25 + package quic import ( @@ -12,10 +14,14 @@ import ( "errors" "net/netip" "testing" + "testing/synctest" "time" ) func TestStatelessResetClientSendsStatelessResetTokenTransportParameter(t *testing.T) { + synctest.Test(t, testStatelessResetClientSendsStatelessResetTokenTransportParameter) +} +func testStatelessResetClientSendsStatelessResetTokenTransportParameter(t *testing.T) { // "[The stateless_reset_token] transport parameter MUST NOT be sent by a client [...]" // https://www.rfc-editor.org/rfc/rfc9000#section-18.2-4.6.1 resetToken := testPeerStatelessResetToken(0) @@ -61,6 +67,9 @@ func newDatagramForReset(cid []byte, size int, addr netip.AddrPort) *datagram { } func TestStatelessResetSentSizes(t *testing.T) { + synctest.Test(t, testStatelessResetSentSizes) +} +func testStatelessResetSentSizes(t *testing.T) { config := &Config{ TLSConfig: newTestTLSConfig(serverSide), StatelessResetKey: testStatelessResetKey, @@ -126,6 +135,9 @@ func TestStatelessResetSentSizes(t *testing.T) { } func TestStatelessResetSuccessfulNewConnectionID(t *testing.T) { + synctest.Test(t, testStatelessResetSuccessfulNewConnectionID) +} +func testStatelessResetSuccessfulNewConnectionID(t *testing.T) { // "[...] Stateless Reset Token field values from [...] NEW_CONNECTION_ID frames [...]" // https://www.rfc-editor.org/rfc/rfc9000#section-10.3.1-1 qr := &qlogRecord{} @@ -155,7 +167,7 @@ func TestStatelessResetSuccessfulNewConnectionID(t *testing.T) { t.Errorf("conn.Wait() = %v, want errStatelessReset", err) } tc.wantIdle("closed connection is idle in draining") - tc.advance(1 * time.Second) // long enough to exit the draining state + time.Sleep(1 * time.Second) // long enough to exit the draining state tc.wantIdle("closed connection is idle after draining") qr.wantEvents(t, jsonEvent{ @@ -167,6 +179,9 @@ func TestStatelessResetSuccessfulNewConnectionID(t *testing.T) { } func TestStatelessResetSuccessfulTransportParameter(t *testing.T) { + synctest.Test(t, testStatelessResetSuccessfulTransportParameter) +} +func testStatelessResetSuccessfulTransportParameter(t *testing.T) { // "[...] Stateless Reset Token field values from [...] // the server's transport parameters [...]" // https://www.rfc-editor.org/rfc/rfc9000#section-10.3.1-1 @@ -229,7 +244,7 @@ func TestStatelessResetSuccessfulPrefix(t *testing.T) { }, testLocalConnID(0)...), size: 100, }} { - t.Run(test.name, func(t *testing.T) { + synctestSubtest(t, test.name, func(t *testing.T) { resetToken := testPeerStatelessResetToken(0) tc := newTestConn(t, clientSide, func(p *transportParameters) { p.statelessResetToken = resetToken[:] @@ -252,6 +267,9 @@ func TestStatelessResetSuccessfulPrefix(t *testing.T) { } func TestStatelessResetRetiredConnID(t *testing.T) { + synctest.Test(t, testStatelessResetRetiredConnID) +} +func testStatelessResetRetiredConnID(t *testing.T) { // "An endpoint MUST NOT check for any stateless reset tokens [...] // for connection IDs that have been retired." // https://www.rfc-editor.org/rfc/rfc9000#section-10.3.1-3 diff --git a/quic/stream.go b/quic/stream.go index b20cfe7f..4c632079 100644 --- a/quic/stream.go +++ b/quic/stream.go @@ -236,7 +236,7 @@ func (s *Stream) Read(b []byte) (n int, err error) { s.inbufoff += n return n, nil } - if err := s.ingate.waitAndLock(s.inctx, s.conn.testHooks); err != nil { + if err := s.ingate.waitAndLock(s.inctx); err != nil { return 0, err } if s.inbufoff > 0 { @@ -350,7 +350,7 @@ func (s *Stream) Write(b []byte) (n int, err error) { if len(b) > 0 && !canWrite { // Our send buffer is full. Wait for the peer to ack some data. s.outUnlock() - if err := s.outgate.waitAndLock(s.outctx, s.conn.testHooks); err != nil { + if err := s.outgate.waitAndLock(s.outctx); err != nil { return n, err } // Successfully returning from waitAndLockGate means we are no longer diff --git a/quic/stream_limits.go b/quic/stream_limits.go index ed31c365..f1abcae9 100644 --- a/quic/stream_limits.go +++ b/quic/stream_limits.go @@ -29,7 +29,7 @@ func (lim *localStreamLimits) init() { // open creates a new local stream, blocking until MAX_STREAMS quota is available. func (lim *localStreamLimits) open(ctx context.Context, c *Conn) (num int64, err error) { // TODO: Send a STREAMS_BLOCKED when blocked. - if err := lim.gate.waitAndLock(ctx, c.testHooks); err != nil { + if err := lim.gate.waitAndLock(ctx); err != nil { return 0, err } if lim.opened < 0 { diff --git a/quic/stream_limits_test.go b/quic/stream_limits_test.go index ad634113..d62b29bb 100644 --- a/quic/stream_limits_test.go +++ b/quic/stream_limits_test.go @@ -2,19 +2,22 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +//go:build go1.25 + package quic import ( "context" "crypto/tls" "testing" + "testing/synctest" ) func TestStreamLimitNewStreamBlocked(t *testing.T) { // "An endpoint that receives a frame with a stream ID exceeding the limit // it has sent MUST treat this as a connection error of type STREAM_LIMIT_ERROR [...]" // https://www.rfc-editor.org/rfc/rfc9000#section-4.6-3 - testStreamTypes(t, "", func(t *testing.T, styp streamType) { + testStreamTypesSynctest(t, "", func(t *testing.T, styp streamType) { ctx := canceledContext() tc := newTestConn(t, clientSide, permissiveTransportParameters, @@ -46,7 +49,7 @@ func TestStreamLimitNewStreamBlocked(t *testing.T) { func TestStreamLimitMaxStreamsDecreases(t *testing.T) { // "MAX_STREAMS frames that do not increase the stream limit MUST be ignored." // https://www.rfc-editor.org/rfc/rfc9000#section-4.6-4 - testStreamTypes(t, "", func(t *testing.T, styp streamType) { + testStreamTypesSynctest(t, "", func(t *testing.T, styp streamType) { ctx := canceledContext() tc := newTestConn(t, clientSide, permissiveTransportParameters, @@ -77,7 +80,7 @@ func TestStreamLimitMaxStreamsDecreases(t *testing.T) { } func TestStreamLimitViolated(t *testing.T) { - testStreamTypes(t, "", func(t *testing.T, styp streamType) { + testStreamTypesSynctest(t, "", func(t *testing.T, styp streamType) { tc := newTestConn(t, serverSide, func(c *Config) { if styp == bidiStream { @@ -104,7 +107,7 @@ func TestStreamLimitViolated(t *testing.T) { } func TestStreamLimitImplicitStreams(t *testing.T) { - testStreamTypes(t, "", func(t *testing.T, styp streamType) { + testStreamTypesSynctest(t, "", func(t *testing.T, styp streamType) { tc := newTestConn(t, serverSide, func(c *Config) { c.MaxBidiRemoteStreams = 1 << 60 @@ -152,7 +155,7 @@ func TestStreamLimitMaxStreamsTransportParameterTooLarge(t *testing.T) { // a value greater than 2^60 [...] the connection MUST be closed // immediately with a connection error of type TRANSPORT_PARAMETER_ERROR [...]" // https://www.rfc-editor.org/rfc/rfc9000#section-4.6-2 - testStreamTypes(t, "", func(t *testing.T, styp streamType) { + testStreamTypesSynctest(t, "", func(t *testing.T, styp streamType) { tc := newTestConn(t, serverSide, func(p *transportParameters) { if styp == bidiStream { @@ -177,7 +180,7 @@ func TestStreamLimitMaxStreamsFrameTooLarge(t *testing.T) { // greater than 2^60 [...] the connection MUST be closed immediately // with a connection error [...] of type FRAME_ENCODING_ERROR [...]" // https://www.rfc-editor.org/rfc/rfc9000#section-4.6-2 - testStreamTypes(t, "", func(t *testing.T, styp streamType) { + testStreamTypesSynctest(t, "", func(t *testing.T, styp streamType) { tc := newTestConn(t, serverSide) tc.handshake() tc.writeFrames(packetTypeInitial, @@ -197,7 +200,7 @@ func TestStreamLimitMaxStreamsFrameTooLarge(t *testing.T) { } func TestStreamLimitSendUpdatesMaxStreams(t *testing.T) { - testStreamTypes(t, "", func(t *testing.T, styp streamType) { + testStreamTypesSynctest(t, "", func(t *testing.T, styp streamType) { tc := newTestConn(t, serverSide, func(c *Config) { if styp == uniStream { c.MaxUniRemoteStreams = 4 @@ -236,6 +239,9 @@ func TestStreamLimitSendUpdatesMaxStreams(t *testing.T) { } func TestStreamLimitStopSendingDoesNotUpdateMaxStreams(t *testing.T) { + synctest.Test(t, testStreamLimitStopSendingDoesNotUpdateMaxStreams) +} +func testStreamLimitStopSendingDoesNotUpdateMaxStreams(t *testing.T) { tc, s := newTestConnAndRemoteStream(t, serverSide, bidiStream, func(c *Config) { c.MaxBidiRemoteStreams = 1 }) diff --git a/quic/stream_test.go b/quic/stream_test.go index 4119cc1e..67d17f65 100644 --- a/quic/stream_test.go +++ b/quic/stream_test.go @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +//go:build go1.25 + package quic import ( @@ -13,12 +15,13 @@ import ( "io" "strings" "testing" + "testing/synctest" "golang.org/x/net/internal/quic/quicwire" ) func TestStreamWriteBlockedByOutputBuffer(t *testing.T) { - testStreamTypes(t, "", func(t *testing.T, styp streamType) { + testStreamTypesSynctest(t, "", func(t *testing.T, styp streamType) { want := []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} const writeBufferSize = 4 tc := newTestConn(t, clientSide, permissiveTransportParameters, func(c *Config) { @@ -79,7 +82,7 @@ func TestStreamWriteBlockedByOutputBuffer(t *testing.T) { } func TestStreamWriteBlockedByStreamFlowControl(t *testing.T) { - testStreamTypes(t, "", func(t *testing.T, styp streamType) { + testStreamTypesSynctest(t, "", func(t *testing.T, styp streamType) { ctx := canceledContext() want := []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} tc := newTestConn(t, clientSide, func(p *transportParameters) { @@ -149,7 +152,7 @@ func TestStreamIgnoresMaxStreamDataReduction(t *testing.T) { // "A sender MUST ignore any MAX_STREAM_DATA [...] frames that // do not increase flow control limits." // https://www.rfc-editor.org/rfc/rfc9000#section-4.1-9 - testStreamTypes(t, "", func(t *testing.T, styp streamType) { + testStreamTypesSynctest(t, "", func(t *testing.T, styp streamType) { ctx := canceledContext() want := []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} tc := newTestConn(t, clientSide, func(p *transportParameters) { @@ -218,7 +221,7 @@ func TestStreamIgnoresMaxStreamDataReduction(t *testing.T) { } func TestStreamWriteBlockedByWriteBufferLimit(t *testing.T) { - testStreamTypes(t, "", func(t *testing.T, styp streamType) { + testStreamTypesSynctest(t, "", func(t *testing.T, styp streamType) { want := []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} const maxWriteBuffer = 4 tc := newTestConn(t, clientSide, func(p *transportParameters) { @@ -392,7 +395,7 @@ func TestStreamReceive(t *testing.T) { wantEOF: true, }}, }} { - testStreamTypes(t, test.name, func(t *testing.T, styp streamType) { + testStreamTypesSynctest(t, test.name, func(t *testing.T, styp streamType) { tc := newTestConn(t, serverSide) tc.handshake() sid := newStreamID(clientSide, styp, 0) @@ -439,7 +442,7 @@ func TestStreamReceive(t *testing.T) { } func TestStreamReceiveExtendsStreamWindow(t *testing.T) { - testStreamTypes(t, "", func(t *testing.T, styp streamType) { + testStreamTypesSynctest(t, "", func(t *testing.T, styp streamType) { const maxWindowSize = 20 ctx := canceledContext() tc := newTestConn(t, serverSide, func(c *Config) { @@ -484,7 +487,7 @@ func TestStreamReceiveViolatesStreamDataLimit(t *testing.T) { // "A receiver MUST close the connection with an error of type FLOW_CONTROL_ERROR if // the sender violates the advertised [...] stream data limits [...]" // https://www.rfc-editor.org/rfc/rfc9000#section-4.1-8 - testStreamTypes(t, "", func(t *testing.T, styp streamType) { + testStreamTypesSynctest(t, "", func(t *testing.T, styp streamType) { const maxStreamData = 10 for _, test := range []struct { off int64 @@ -521,7 +524,7 @@ func TestStreamReceiveViolatesStreamDataLimit(t *testing.T) { } func TestStreamReceiveDuplicateDataDoesNotViolateLimits(t *testing.T) { - testStreamTypes(t, "", func(t *testing.T, styp streamType) { + testStreamTypesSynctest(t, "", func(t *testing.T, styp streamType) { const maxData = 10 tc := newTestConn(t, serverSide, func(c *Config) { // TODO: Add connection-level maximum data here as well. @@ -544,7 +547,7 @@ func TestStreamReceiveEmptyEOF(t *testing.T) { // A stream receives some data, we read a byte of that data // (causing the rest to be pulled into the s.inbuf buffer), // and then we receive a FIN with no additional data. - testStreamTypes(t, "", func(t *testing.T, styp streamType) { + testStreamTypesSynctest(t, "", func(t *testing.T, styp streamType) { tc, s := newTestConnAndRemoteStream(t, serverSide, styp, permissiveTransportParameters) want := []byte{1, 2, 3} tc.writeFrames(packetType1RTT, debugFrameStream{ @@ -568,7 +571,7 @@ func TestStreamReceiveEmptyEOF(t *testing.T) { func TestStreamReadByteFromOneByteStream(t *testing.T) { // ReadByte on the only byte of a stream should not return an error. - testStreamTypes(t, "", func(t *testing.T, styp streamType) { + testStreamTypesSynctest(t, "", func(t *testing.T, styp streamType) { tc, s := newTestConnAndRemoteStream(t, serverSide, styp, permissiveTransportParameters) want := byte(1) tc.writeFrames(packetType1RTT, debugFrameStream{ @@ -608,7 +611,7 @@ func finalSizeTest(t *testing.T, wantErr transportError, f func(tc *testConn, si }) }, }} { - t.Run(test.name, func(t *testing.T) { + synctestSubtest(t, test.name, func(t *testing.T) { tc := newTestConn(t, serverSide, opts...) tc.handshake() sid := newStreamID(clientSide, styp, 0) @@ -662,7 +665,7 @@ func TestStreamDataBeyondFinalSize(t *testing.T) { // "A receiver SHOULD treat receipt of data at or beyond // the final size as an error of type FINAL_SIZE_ERROR [...]" // https://www.rfc-editor.org/rfc/rfc9000#section-4.5-5 - testStreamTypes(t, "", func(t *testing.T, styp streamType) { + testStreamTypesSynctest(t, "", func(t *testing.T, styp streamType) { tc := newTestConn(t, serverSide) tc.handshake() sid := newStreamID(clientSide, styp, 0) @@ -688,7 +691,7 @@ func TestStreamDataBeyondFinalSize(t *testing.T) { } func TestStreamReceiveUnblocksReader(t *testing.T) { - testStreamTypes(t, "", func(t *testing.T, styp streamType) { + testStreamTypesSynctest(t, "", func(t *testing.T, styp streamType) { tc := newTestConn(t, serverSide) tc.handshake() want := []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} @@ -746,7 +749,7 @@ func TestStreamReceiveUnblocksReader(t *testing.T) { // It then sends the returned frame (STREAM, STREAM_DATA_BLOCKED, etc.) // to the conn and expects a STREAM_STATE_ERROR. func testStreamSendFrameInvalidState(t *testing.T, f func(sid streamID) debugFrame) { - testSides(t, "stream_not_created", func(t *testing.T, side connSide) { + testSidesSynctest(t, "stream_not_created", func(t *testing.T, side connSide) { tc := newTestConn(t, side, permissiveTransportParameters) tc.handshake() tc.writeFrames(packetType1RTT, f(newStreamID(side, bidiStream, 0))) @@ -755,7 +758,7 @@ func testStreamSendFrameInvalidState(t *testing.T, f func(sid streamID) debugFra code: errStreamState, }) }) - testSides(t, "uni_stream", func(t *testing.T, side connSide) { + testSidesSynctest(t, "uni_stream", func(t *testing.T, side connSide) { ctx := canceledContext() tc := newTestConn(t, side, permissiveTransportParameters) tc.handshake() @@ -823,7 +826,7 @@ func TestStreamDataBlockedInvalidState(t *testing.T) { // It then sends the returned frame (MAX_STREAM_DATA, STOP_SENDING, etc.) // to the conn and expects a STREAM_STATE_ERROR. func testStreamReceiveFrameInvalidState(t *testing.T, f func(sid streamID) debugFrame) { - testSides(t, "stream_not_created", func(t *testing.T, side connSide) { + testSidesSynctest(t, "stream_not_created", func(t *testing.T, side connSide) { tc := newTestConn(t, side) tc.handshake() tc.writeFrames(packetType1RTT, f(newStreamID(side, bidiStream, 0))) @@ -832,7 +835,7 @@ func testStreamReceiveFrameInvalidState(t *testing.T, f func(sid streamID) debug code: errStreamState, }) }) - testSides(t, "uni_stream", func(t *testing.T, side connSide) { + testSidesSynctest(t, "uni_stream", func(t *testing.T, side connSide) { tc := newTestConn(t, side) tc.handshake() tc.writeFrames(packetType1RTT, f(newStreamID(side.peer(), uniStream, 0))) @@ -873,6 +876,9 @@ func TestStreamMaxStreamDataInvalidState(t *testing.T) { } func TestStreamOffsetTooLarge(t *testing.T) { + synctest.Test(t, testStreamOffsetTooLarge) +} +func testStreamOffsetTooLarge(t *testing.T) { // "Receipt of a frame that exceeds [2^62-1] MUST be treated as a // connection error of type FRAME_ENCODING_ERROR or FLOW_CONTROL_ERROR." // https://www.rfc-editor.org/rfc/rfc9000.html#section-19.8-9 @@ -894,6 +900,9 @@ func TestStreamOffsetTooLarge(t *testing.T) { } func TestStreamReadFromWriteOnlyStream(t *testing.T) { + synctest.Test(t, testStreamReadFromWriteOnlyStream) +} +func testStreamReadFromWriteOnlyStream(t *testing.T) { _, s := newTestConnAndLocalStream(t, serverSide, uniStream, permissiveTransportParameters) buf := make([]byte, 10) wantErr := "read from write-only stream" @@ -903,6 +912,9 @@ func TestStreamReadFromWriteOnlyStream(t *testing.T) { } func TestStreamWriteToReadOnlyStream(t *testing.T) { + synctest.Test(t, testStreamWriteToReadOnlyStream) +} +func testStreamWriteToReadOnlyStream(t *testing.T) { _, s := newTestConnAndRemoteStream(t, serverSide, uniStream) buf := make([]byte, 10) wantErr := "write to read-only stream" @@ -912,6 +924,9 @@ func TestStreamWriteToReadOnlyStream(t *testing.T) { } func TestStreamReadFromClosedStream(t *testing.T) { + synctest.Test(t, testStreamReadFromClosedStream) +} +func testStreamReadFromClosedStream(t *testing.T) { tc, s := newTestConnAndRemoteStream(t, serverSide, bidiStream, permissiveTransportParameters) s.CloseRead() tc.wantFrame("CloseRead sends a STOP_SENDING frame", @@ -934,6 +949,9 @@ func TestStreamReadFromClosedStream(t *testing.T) { } func TestStreamCloseReadWithAllDataReceived(t *testing.T) { + synctest.Test(t, testStreamCloseReadWithAllDataReceived) +} +func testStreamCloseReadWithAllDataReceived(t *testing.T) { tc, s := newTestConnAndRemoteStream(t, serverSide, bidiStream, permissiveTransportParameters) tc.writeFrames(packetType1RTT, debugFrameStream{ id: s.id, @@ -950,6 +968,9 @@ func TestStreamCloseReadWithAllDataReceived(t *testing.T) { } func TestStreamWriteToClosedStream(t *testing.T) { + synctest.Test(t, testStreamWriteToClosedStream) +} +func testStreamWriteToClosedStream(t *testing.T) { tc, s := newTestConnAndLocalStream(t, serverSide, bidiStream, permissiveTransportParameters) s.CloseWrite() tc.wantFrame("stream is opened after being closed", @@ -966,6 +987,9 @@ func TestStreamWriteToClosedStream(t *testing.T) { } func TestStreamResetBlockedStream(t *testing.T) { + synctest.Test(t, testStreamResetBlockedStream) +} +func testStreamResetBlockedStream(t *testing.T) { tc, s := newTestConnAndLocalStream(t, serverSide, bidiStream, permissiveTransportParameters, func(c *Config) { c.MaxStreamWriteBufferSize = 4 @@ -1002,6 +1026,9 @@ func TestStreamResetBlockedStream(t *testing.T) { } func TestStreamWriteMoreThanOnePacketOfData(t *testing.T) { + synctest.Test(t, testStreamWriteMoreThanOnePacketOfData) +} +func testStreamWriteMoreThanOnePacketOfData(t *testing.T) { tc, s := newTestConnAndLocalStream(t, serverSide, uniStream, func(p *transportParameters) { p.initialMaxStreamsUni = 1 p.initialMaxData = 1 << 20 @@ -1038,6 +1065,9 @@ func TestStreamWriteMoreThanOnePacketOfData(t *testing.T) { } func TestStreamCloseWaitsForAcks(t *testing.T) { + synctest.Test(t, testStreamCloseWaitsForAcks) +} +func testStreamCloseWaitsForAcks(t *testing.T) { tc, s := newTestConnAndLocalStream(t, serverSide, uniStream, permissiveTransportParameters) data := make([]byte, 100) s.Write(data) @@ -1071,6 +1101,9 @@ func TestStreamCloseWaitsForAcks(t *testing.T) { } func TestStreamCloseReadOnly(t *testing.T) { + synctest.Test(t, testStreamCloseReadOnly) +} +func testStreamCloseReadOnly(t *testing.T) { tc, s := newTestConnAndRemoteStream(t, serverSide, uniStream, permissiveTransportParameters) if err := s.Close(); err != nil { t.Errorf("s.Close() = %v, want nil", err) @@ -1103,10 +1136,10 @@ func TestStreamCloseUnblocked(t *testing.T) { name: "stream reset", unblock: func(tc *testConn, s *Stream) { s.Reset(0) - tc.wait() // wait for test conn to process the Reset + synctest.Wait() // wait for test conn to process the Reset }, }} { - t.Run(test.name, func(t *testing.T) { + synctestSubtest(t, test.name, func(t *testing.T) { tc, s := newTestConnAndLocalStream(t, serverSide, uniStream, permissiveTransportParameters) data := make([]byte, 100) s.Write(data) @@ -1148,6 +1181,9 @@ func TestStreamCloseUnblocked(t *testing.T) { } func TestStreamCloseWriteWhenBlockedByStreamFlowControl(t *testing.T) { + synctest.Test(t, testStreamCloseWriteWhenBlockedByStreamFlowControl) +} +func testStreamCloseWriteWhenBlockedByStreamFlowControl(t *testing.T) { tc, s := newTestConnAndLocalStream(t, serverSide, uniStream, permissiveTransportParameters, func(p *transportParameters) { //p.initialMaxData = 0 @@ -1185,7 +1221,7 @@ func TestStreamCloseWriteWhenBlockedByStreamFlowControl(t *testing.T) { } func TestStreamPeerResetsWithUnreadAndUnsentData(t *testing.T) { - testStreamTypes(t, "", func(t *testing.T, styp streamType) { + testStreamTypesSynctest(t, "", func(t *testing.T, styp streamType) { tc, s := newTestConnAndRemoteStream(t, serverSide, styp) data := []byte{0, 1, 2, 3, 4, 5, 6, 7} tc.writeFrames(packetType1RTT, debugFrameStream{ @@ -1210,7 +1246,7 @@ func TestStreamPeerResetsWithUnreadAndUnsentData(t *testing.T) { } func TestStreamPeerResetWakesBlockedRead(t *testing.T) { - testStreamTypes(t, "", func(t *testing.T, styp streamType) { + testStreamTypesSynctest(t, "", func(t *testing.T, styp streamType) { tc, s := newTestConnAndRemoteStream(t, serverSide, styp) reader := runAsync(tc, func(ctx context.Context) (int, error) { s.SetReadContext(ctx) @@ -1231,7 +1267,7 @@ func TestStreamPeerResetWakesBlockedRead(t *testing.T) { } func TestStreamPeerResetFollowedByData(t *testing.T) { - testStreamTypes(t, "", func(t *testing.T, styp streamType) { + testStreamTypesSynctest(t, "", func(t *testing.T, styp streamType) { tc, s := newTestConnAndRemoteStream(t, serverSide, styp) tc.writeFrames(packetType1RTT, debugFrameResetStream{ id: s.id, @@ -1256,6 +1292,9 @@ func TestStreamPeerResetFollowedByData(t *testing.T) { } func TestStreamResetInvalidCode(t *testing.T) { + synctest.Test(t, testStreamResetInvalidCode) +} +func testStreamResetInvalidCode(t *testing.T) { tc, s := newTestConnAndLocalStream(t, serverSide, uniStream, permissiveTransportParameters) s.Reset(1 << 62) tc.wantFrame("reset with invalid code sends a RESET_STREAM anyway", @@ -1268,6 +1307,9 @@ func TestStreamResetInvalidCode(t *testing.T) { } func TestStreamResetReceiveOnly(t *testing.T) { + synctest.Test(t, testStreamResetReceiveOnly) +} +func testStreamResetReceiveOnly(t *testing.T) { tc, s := newTestConnAndRemoteStream(t, serverSide, uniStream) s.Reset(0) tc.wantIdle("resetting a receive-only stream has no effect") @@ -1277,7 +1319,7 @@ func TestStreamPeerStopSendingForActiveStream(t *testing.T) { // "An endpoint that receives a STOP_SENDING frame MUST send a RESET_STREAM frame if // the stream is in the "Ready" or "Send" state." // https://www.rfc-editor.org/rfc/rfc9000#section-3.5-4 - testStreamTypes(t, "", func(t *testing.T, styp streamType) { + testStreamTypesSynctest(t, "", func(t *testing.T, styp streamType) { tc, s := newTestConnAndLocalStream(t, serverSide, styp, permissiveTransportParameters) for i := 0; i < 4; i++ { s.Write([]byte{byte(i)}) @@ -1309,6 +1351,9 @@ func TestStreamPeerStopSendingForActiveStream(t *testing.T) { } func TestStreamReceiveDataBlocked(t *testing.T) { + synctest.Test(t, testStreamReceiveDataBlocked) +} +func testStreamReceiveDataBlocked(t *testing.T) { tc := newTestConn(t, serverSide, permissiveTransportParameters) tc.handshake() tc.ignoreFrame(frameTypeAck) @@ -1326,7 +1371,7 @@ func TestStreamReceiveDataBlocked(t *testing.T) { } func TestStreamFlushExplicit(t *testing.T) { - testStreamTypes(t, "", func(t *testing.T, styp streamType) { + testStreamTypesSynctest(t, "", func(t *testing.T, styp streamType) { tc, s := newTestConnAndLocalStream(t, clientSide, styp, permissiveTransportParameters) want := []byte{0, 1, 2, 3} n, err := s.Write(want) @@ -1344,6 +1389,9 @@ func TestStreamFlushExplicit(t *testing.T) { } func TestStreamFlushClosedStream(t *testing.T) { + synctest.Test(t, testStreamFlushClosedStream) +} +func testStreamFlushClosedStream(t *testing.T) { _, s := newTestConnAndLocalStream(t, clientSide, bidiStream, permissiveTransportParameters) s.Close() @@ -1353,6 +1401,9 @@ func TestStreamFlushClosedStream(t *testing.T) { } func TestStreamFlushResetStream(t *testing.T) { + synctest.Test(t, testStreamFlushResetStream) +} +func testStreamFlushResetStream(t *testing.T) { _, s := newTestConnAndLocalStream(t, clientSide, bidiStream, permissiveTransportParameters) s.Reset(0) @@ -1362,6 +1413,9 @@ func TestStreamFlushResetStream(t *testing.T) { } func TestStreamFlushStreamAfterPeerStopSending(t *testing.T) { + synctest.Test(t, testStreamFlushStreamAfterPeerStopSending) +} +func testStreamFlushStreamAfterPeerStopSending(t *testing.T) { tc, s := newTestConnAndLocalStream(t, clientSide, bidiStream, permissiveTransportParameters) s.Flush() // create the stream @@ -1381,6 +1435,9 @@ func TestStreamFlushStreamAfterPeerStopSending(t *testing.T) { } func TestStreamErrorsAfterConnectionClosed(t *testing.T) { + synctest.Test(t, testStreamErrorsAfterConnectionClosed) +} +func testStreamErrorsAfterConnectionClosed(t *testing.T) { tc, s := newTestConnAndLocalStream(t, clientSide, bidiStream, permissiveTransportParameters) wantErr := &ApplicationError{Code: 42} @@ -1399,7 +1456,7 @@ func TestStreamErrorsAfterConnectionClosed(t *testing.T) { } func TestStreamFlushImplicitExact(t *testing.T) { - testStreamTypes(t, "", func(t *testing.T, styp streamType) { + testStreamTypesSynctest(t, "", func(t *testing.T, styp streamType) { const writeBufferSize = 4 tc, s := newTestConnAndLocalStream(t, clientSide, styp, permissiveTransportParameters, @@ -1429,7 +1486,7 @@ func TestStreamFlushImplicitExact(t *testing.T) { } func TestStreamFlushImplicitLargerThanBuffer(t *testing.T) { - testStreamTypes(t, "", func(t *testing.T, styp streamType) { + testStreamTypesSynctest(t, "", func(t *testing.T, styp streamType) { const writeBufferSize = 4 tc, s := newTestConnAndLocalStream(t, clientSide, styp, permissiveTransportParameters, diff --git a/quic/tls_test.go b/quic/tls_test.go index 08c75dda..0818c688 100644 --- a/quic/tls_test.go +++ b/quic/tls_test.go @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +//go:build go1.25 + package quic import ( @@ -9,12 +11,12 @@ import ( "crypto/x509" "errors" "testing" + "testing/synctest" "time" ) // handshake executes the handshake. func (tc *testConn) handshake() { - tc.t.Helper() if *testVV { *testVV = false defer func() { @@ -32,16 +34,16 @@ func (tc *testConn) handshake() { i := 0 for { if i == len(dgrams)-1 { - want := tc.endpoint.now.Add(maxAckDelay - timerGranularity) + want := time.Now().Add(maxAckDelay - timerGranularity) if tc.conn.side == clientSide { - if !tc.timer.Equal(want) { - t.Fatalf("want timer = %v (max_ack_delay), got %v", want, tc.timer) + if got := tc.nextEvent(); !got.Equal(want) { + t.Fatalf("want timer = %v (max_ack_delay), got %v", want, got) } if got := tc.readDatagram(); got != nil { t.Fatalf("client unexpectedly sent: %v", got) } } - tc.advanceTo(want) + time.Sleep(time.Until(want)) } // Check that we're sending exactly the data we expect. @@ -308,20 +310,29 @@ func (tc *testConn) uncheckedHandshake() { } func TestConnClientHandshake(t *testing.T) { + synctest.Test(t, testConnClientHandshake) +} +func testConnClientHandshake(t *testing.T) { tc := newTestConn(t, clientSide) tc.handshake() - tc.advance(1 * time.Second) + time.Sleep(1 * time.Second) tc.wantIdle("no packets should be sent by an idle conn after the handshake") } func TestConnServerHandshake(t *testing.T) { + synctest.Test(t, testConnServerHandshake) +} +func testConnServerHandshake(t *testing.T) { tc := newTestConn(t, serverSide) tc.handshake() - tc.advance(1 * time.Second) + time.Sleep(1 * time.Second) tc.wantIdle("no packets should be sent by an idle conn after the handshake") } func TestConnKeysDiscardedClient(t *testing.T) { + synctest.Test(t, testConnKeysDiscardedClient) +} +func testConnKeysDiscardedClient(t *testing.T) { tc := newTestConn(t, clientSide) tc.ignoreFrame(frameTypeAck) @@ -370,6 +381,9 @@ func TestConnKeysDiscardedClient(t *testing.T) { } func TestConnKeysDiscardedServer(t *testing.T) { + synctest.Test(t, testConnKeysDiscardedServer) +} +func testConnKeysDiscardedServer(t *testing.T) { tc := newTestConn(t, serverSide) tc.ignoreFrame(frameTypeAck) @@ -425,6 +439,9 @@ func TestConnKeysDiscardedServer(t *testing.T) { } func TestConnInvalidCryptoData(t *testing.T) { + synctest.Test(t, testConnInvalidCryptoData) +} +func testConnInvalidCryptoData(t *testing.T) { tc := newTestConn(t, clientSide) tc.ignoreFrame(frameTypeAck) @@ -455,6 +472,9 @@ func TestConnInvalidCryptoData(t *testing.T) { } func TestConnInvalidPeerCertificate(t *testing.T) { + synctest.Test(t, testConnInvalidPeerCertificate) +} +func testConnInvalidPeerCertificate(t *testing.T) { tc := newTestConn(t, clientSide, func(c *tls.Config) { c.VerifyPeerCertificate = func([][]byte, [][]*x509.Certificate) error { return errors.New("I will not buy this certificate. It is scratched.") @@ -481,6 +501,9 @@ func TestConnInvalidPeerCertificate(t *testing.T) { } func TestConnHandshakeDoneSentToServer(t *testing.T) { + synctest.Test(t, testConnHandshakeDoneSentToServer) +} +func testConnHandshakeDoneSentToServer(t *testing.T) { tc := newTestConn(t, serverSide) tc.handshake() @@ -493,6 +516,9 @@ func TestConnHandshakeDoneSentToServer(t *testing.T) { } func TestConnCryptoDataOutOfOrder(t *testing.T) { + synctest.Test(t, testConnCryptoDataOutOfOrder) +} +func testConnCryptoDataOutOfOrder(t *testing.T) { tc := newTestConn(t, clientSide) tc.ignoreFrame(frameTypeAck) @@ -531,6 +557,9 @@ func TestConnCryptoDataOutOfOrder(t *testing.T) { } func TestConnCryptoBufferSizeExceeded(t *testing.T) { + synctest.Test(t, testConnCryptoBufferSizeExceeded) +} +func testConnCryptoBufferSizeExceeded(t *testing.T) { tc := newTestConn(t, clientSide) tc.ignoreFrame(frameTypeAck) @@ -550,6 +579,9 @@ func TestConnCryptoBufferSizeExceeded(t *testing.T) { } func TestConnAEADLimitReached(t *testing.T) { + synctest.Test(t, testConnAEADLimitReached) +} +func testConnAEADLimitReached(t *testing.T) { // "[...] endpoints MUST count the number of received packets that // fail authentication during the lifetime of a connection. // If the total number of received packets that fail authentication [...] @@ -590,7 +622,7 @@ func TestConnAEADLimitReached(t *testing.T) { tc.conn.sendMsg(&datagram{ b: invalid, }) - tc.wait() + synctest.Wait() } // Set the conn's auth failure count to just before the AEAD integrity limit. @@ -610,11 +642,14 @@ func TestConnAEADLimitReached(t *testing.T) { }) tc.writeFrames(packetType1RTT, debugFramePing{}) - tc.advance(1 * time.Second) + time.Sleep(1 * time.Second) tc.wantIdle("auth failures at limit: conn does not process additional packets") } func TestConnKeysDiscardedWithExcessCryptoData(t *testing.T) { + synctest.Test(t, testConnKeysDiscardedWithExcessCryptoData) +} +func testConnKeysDiscardedWithExcessCryptoData(t *testing.T) { tc := newTestConn(t, serverSide, permissiveTransportParameters) tc.ignoreFrame(frameTypeAck) tc.ignoreFrame(frameTypeNewConnectionID) diff --git a/quic/version_test.go b/quic/version_test.go index 60d83078..ac054a83 100644 --- a/quic/version_test.go +++ b/quic/version_test.go @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +//go:build go1.25 + package quic import ( @@ -9,9 +11,13 @@ import ( "context" "crypto/tls" "testing" + "testing/synctest" ) func TestVersionNegotiationServerReceivesUnknownVersion(t *testing.T) { + synctest.Test(t, testVersionNegotiationServerReceivesUnknownVersion) +} +func testVersionNegotiationServerReceivesUnknownVersion(t *testing.T) { config := &Config{ TLSConfig: newTestTLSConfig(serverSide), } @@ -55,6 +61,9 @@ func TestVersionNegotiationServerReceivesUnknownVersion(t *testing.T) { } func TestVersionNegotiationClientAborts(t *testing.T) { + synctest.Test(t, testVersionNegotiationClientAborts) +} +func testVersionNegotiationClientAborts(t *testing.T) { tc := newTestConn(t, clientSide) p := tc.readPacket() // client Initial packet tc.endpoint.write(&datagram{ @@ -67,6 +76,9 @@ func TestVersionNegotiationClientAborts(t *testing.T) { } func TestVersionNegotiationClientIgnoresAfterProcessingPacket(t *testing.T) { + synctest.Test(t, testVersionNegotiationClientIgnoresAfterProcessingPacket) +} +func testVersionNegotiationClientIgnoresAfterProcessingPacket(t *testing.T) { tc := newTestConn(t, clientSide) tc.ignoreFrame(frameTypeAck) p := tc.readPacket() // client Initial packet @@ -89,6 +101,9 @@ func TestVersionNegotiationClientIgnoresAfterProcessingPacket(t *testing.T) { } func TestVersionNegotiationClientIgnoresMismatchingSourceConnID(t *testing.T) { + synctest.Test(t, testVersionNegotiationClientIgnoresMismatchingSourceConnID) +} +func testVersionNegotiationClientIgnoresMismatchingSourceConnID(t *testing.T) { tc := newTestConn(t, clientSide) tc.ignoreFrame(frameTypeAck) p := tc.readPacket() // client Initial packet