Skip to content

Commit d1f64cc

Browse files
neildgopherbot
authored andcommitted
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 <nsh@golang.org> Reviewed-by: Nicholas Husin <husin@google.com> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Auto-Submit: Damien Neil <dneil@google.com>
1 parent fff0469 commit d1f64cc

35 files changed

+702
-479
lines changed

quic/bench_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
// Use of this source code is governed by a BSD-style
33
// license that can be found in the LICENSE file.
44

5+
//go:build go1.25
6+
57
package quic
68

79
import (

quic/config_test.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,19 @@
22
// Use of this source code is governed by a BSD-style
33
// license that can be found in the LICENSE file.
44

5+
//go:build go1.25
6+
57
package quic
68

7-
import "testing"
9+
import (
10+
"testing"
11+
"testing/synctest"
12+
)
813

914
func TestConfigTransportParameters(t *testing.T) {
15+
synctest.Test(t, testConfigTransportParameters)
16+
}
17+
func testConfigTransportParameters(t *testing.T) {
1018
const (
1119
wantInitialMaxData = int64(1)
1220
wantInitialMaxStreamData = int64(2)

quic/conn.go

Lines changed: 10 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -69,23 +69,12 @@ type connTestHooks interface {
6969
// init is called after a conn is created.
7070
init(first bool)
7171

72-
// nextMessage is called to request the next event from msgc.
73-
// Used to give tests control of the connection event loop.
74-
nextMessage(msgc chan any, nextTimeout time.Time) (now time.Time, message any)
75-
7672
// handleTLSEvent is called with each TLS event.
7773
handleTLSEvent(tls.QUICEvent)
7874

7975
// newConnID is called to generate a new connection ID.
8076
// Permits tests to generate consistent connection IDs rather than random ones.
8177
newConnID(seq int64) ([]byte, error)
82-
83-
// waitUntil blocks until the until func returns true or the context is done.
84-
// Used to synchronize asynchronous blocking operations in tests.
85-
waitUntil(ctx context.Context, until func() bool) error
86-
87-
// timeNow returns the current time.
88-
timeNow() time.Time
8978
}
9079

9180
// 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
10291
endpoint: e,
10392
config: config,
10493
peerAddr: unmapAddrPort(peerAddr),
105-
msgc: make(chan any, 1),
10694
donec: make(chan struct{}),
10795
peerAckDelayExponent: -1,
10896
}
@@ -299,17 +287,12 @@ func (c *Conn) loop(now time.Time) {
299287
// The connection timer sends a message to the connection loop on expiry.
300288
// We need to give it an expiry when creating it, so set the initial timeout to
301289
// an arbitrary large value. The timer will be reset before this expires (and it
302-
// isn't a problem if it does anyway). Skip creating the timer in tests which
303-
// take control of the connection message loop.
304-
var timer *time.Timer
290+
// isn't a problem if it does anyway).
305291
var lastTimeout time.Time
306-
hooks := c.testHooks
307-
if hooks == nil {
308-
timer = time.AfterFunc(1*time.Hour, func() {
309-
c.sendMsg(timerEvent{})
310-
})
311-
defer timer.Stop()
312-
}
292+
timer := time.AfterFunc(1*time.Hour, func() {
293+
c.sendMsg(timerEvent{})
294+
})
295+
defer timer.Stop()
313296

314297
for c.lifetime.state != connStateDone {
315298
sendTimeout := c.maybeSend(now) // try sending
@@ -326,10 +309,7 @@ func (c *Conn) loop(now time.Time) {
326309
}
327310

328311
var m any
329-
if hooks != nil {
330-
// Tests only: Wait for the test to tell us to continue.
331-
now, m = hooks.nextMessage(c.msgc, nextTimeout)
332-
} else if !nextTimeout.IsZero() && nextTimeout.Before(now) {
312+
if !nextTimeout.IsZero() && nextTimeout.Before(now) {
333313
// A connection timer has expired.
334314
now = time.Now()
335315
m = timerEvent{}
@@ -372,6 +352,9 @@ func (c *Conn) loop(now time.Time) {
372352
case func(time.Time, *Conn):
373353
// Send a func to msgc to run it on the main Conn goroutine
374354
m(now, c)
355+
case func(now, next time.Time, _ *Conn):
356+
// Send a func to msgc to run it on the main Conn goroutine
357+
m(now, nextTimeout, c)
375358
default:
376359
panic(fmt.Sprintf("quic: unrecognized conn message %T", m))
377360
}
@@ -410,31 +393,7 @@ func (c *Conn) runOnLoop(ctx context.Context, f func(now time.Time, c *Conn)) er
410393
defer close(donec)
411394
f(now, c)
412395
}
413-
if c.testHooks != nil {
414-
// In tests, we can't rely on being able to send a message immediately:
415-
// c.msgc might be full, and testConnHooks.nextMessage might be waiting
416-
// for us to block before it processes the next message.
417-
// To avoid a deadlock, we send the message in waitUntil.
418-
// If msgc is empty, the message is buffered.
419-
// If msgc is full, we block and let nextMessage process the queue.
420-
msgc := c.msgc
421-
c.testHooks.waitUntil(ctx, func() bool {
422-
for {
423-
select {
424-
case msgc <- msg:
425-
msgc = nil // send msg only once
426-
case <-donec:
427-
return true
428-
case <-c.donec:
429-
return true
430-
default:
431-
return false
432-
}
433-
}
434-
})
435-
} else {
436-
c.sendMsg(msg)
437-
}
396+
c.sendMsg(msg)
438397
select {
439398
case <-donec:
440399
case <-c.donec:
@@ -444,16 +403,6 @@ func (c *Conn) runOnLoop(ctx context.Context, f func(now time.Time, c *Conn)) er
444403
}
445404

446405
func (c *Conn) waitOnDone(ctx context.Context, ch <-chan struct{}) error {
447-
if c.testHooks != nil {
448-
return c.testHooks.waitUntil(ctx, func() bool {
449-
select {
450-
case <-ch:
451-
return true
452-
default:
453-
}
454-
return false
455-
})
456-
}
457406
// Check the channel before the context.
458407
// We always prefer to return results when available,
459408
// even when provided with an already-canceled context.

quic/conn_async_test.go

Lines changed: 13 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -2,62 +2,40 @@
22
// Use of this source code is governed by a BSD-style
33
// license that can be found in the LICENSE file.
44

5+
//go:build go1.25
6+
57
package quic
68

79
import (
810
"context"
911
"errors"
1012
"fmt"
11-
"path/filepath"
12-
"runtime"
13-
"sync"
13+
"testing/synctest"
1414
)
1515

16-
// asyncTestState permits handling asynchronous operations in a synchronous test.
17-
//
18-
// For example, a test may want to write to a stream and observe that
19-
// STREAM frames are sent with the contents of the write in response
20-
// to MAX_STREAM_DATA frames received from the peer.
21-
// The Stream.Write is an asynchronous operation, but the test is simpler
22-
// if we can start the write, observe the first STREAM frame sent,
23-
// send a MAX_STREAM_DATA frame, observe the next STREAM frame sent, etc.
24-
//
25-
// We do this by instrumenting points where operations can block.
26-
// We start async operations like Write in a goroutine,
27-
// and wait for the operation to either finish or hit a blocking point.
28-
// When the connection event loop is idle, we check a list of
29-
// blocked operations to see if any can be woken.
30-
type asyncTestState struct {
31-
mu sync.Mutex
32-
notify chan struct{}
33-
blocked map[*blockedAsync]struct{}
34-
}
35-
3616
// An asyncOp is an asynchronous operation that results in (T, error).
3717
type asyncOp[T any] struct {
38-
v T
39-
err error
40-
41-
caller string
42-
tc *testConn
18+
v T
19+
err error
4320
donec chan struct{}
4421
cancelFunc context.CancelFunc
4522
}
4623

4724
// cancel cancels the async operation's context, and waits for
4825
// the operation to complete.
4926
func (a *asyncOp[T]) cancel() {
27+
synctest.Wait()
5028
select {
5129
case <-a.donec:
5230
return // already done
5331
default:
5432
}
5533
a.cancelFunc()
56-
<-a.tc.asyncTestState.notify
34+
synctest.Wait()
5735
select {
5836
case <-a.donec:
5937
default:
60-
panic(fmt.Errorf("%v: async op failed to finish after being canceled", a.caller))
38+
panic(fmt.Errorf("async op failed to finish after being canceled"))
6139
}
6240
}
6341

@@ -71,115 +49,30 @@ var errNotDone = errors.New("async op is not done")
7149
// control over the progress of operations, an asyncOp can only
7250
// become done in reaction to the test taking some action.
7351
func (a *asyncOp[T]) result() (v T, err error) {
74-
a.tc.wait()
52+
synctest.Wait()
7553
select {
7654
case <-a.donec:
7755
return a.v, a.err
7856
default:
79-
return v, errNotDone
57+
return a.v, errNotDone
8058
}
8159
}
8260

83-
// A blockedAsync is a blocked async operation.
84-
type blockedAsync struct {
85-
until func() bool // when this returns true, the operation is unblocked
86-
donec chan struct{} // closed when the operation is unblocked
87-
}
88-
89-
type asyncContextKey struct{}
90-
9161
// runAsync starts an asynchronous operation.
9262
//
9363
// The function f should call a blocking function such as
9464
// Stream.Write or Conn.AcceptStream and return its result.
9565
// It must use the provided context.
9666
func runAsync[T any](tc *testConn, f func(context.Context) (T, error)) *asyncOp[T] {
97-
as := &tc.asyncTestState
98-
if as.notify == nil {
99-
as.notify = make(chan struct{})
100-
as.mu.Lock()
101-
as.blocked = make(map[*blockedAsync]struct{})
102-
as.mu.Unlock()
103-
}
104-
_, file, line, _ := runtime.Caller(1)
105-
ctx := context.WithValue(context.Background(), asyncContextKey{}, true)
106-
ctx, cancel := context.WithCancel(ctx)
67+
ctx, cancel := context.WithCancel(tc.t.Context())
10768
a := &asyncOp[T]{
108-
tc: tc,
109-
caller: fmt.Sprintf("%v:%v", filepath.Base(file), line),
11069
donec: make(chan struct{}),
11170
cancelFunc: cancel,
11271
}
11372
go func() {
73+
defer close(a.donec)
11474
a.v, a.err = f(ctx)
115-
close(a.donec)
116-
as.notify <- struct{}{}
11775
}()
118-
tc.t.Cleanup(func() {
119-
if _, err := a.result(); err == errNotDone {
120-
tc.t.Errorf("%v: async operation is still executing at end of test", a.caller)
121-
a.cancel()
122-
}
123-
})
124-
// Wait for the operation to either finish or block.
125-
<-as.notify
126-
tc.wait()
76+
synctest.Wait()
12777
return a
12878
}
129-
130-
// waitUntil waits for a blocked async operation to complete.
131-
// The operation is complete when the until func returns true.
132-
func (as *asyncTestState) waitUntil(ctx context.Context, until func() bool) error {
133-
if until() {
134-
return nil
135-
}
136-
if err := ctx.Err(); err != nil {
137-
// Context has already expired.
138-
return err
139-
}
140-
if ctx.Value(asyncContextKey{}) == nil {
141-
// Context is not one that we've created, and hasn't expired.
142-
// This probably indicates that we've tried to perform a
143-
// blocking operation without using the async test harness here,
144-
// which may have unpredictable results.
145-
panic("blocking async point with unexpected Context")
146-
}
147-
b := &blockedAsync{
148-
until: until,
149-
donec: make(chan struct{}),
150-
}
151-
// Record this as a pending blocking operation.
152-
as.mu.Lock()
153-
as.blocked[b] = struct{}{}
154-
as.mu.Unlock()
155-
// Notify the creator of the operation that we're blocked,
156-
// and wait to be woken up.
157-
as.notify <- struct{}{}
158-
select {
159-
case <-b.donec:
160-
case <-ctx.Done():
161-
return ctx.Err()
162-
}
163-
return nil
164-
}
165-
166-
// wakeAsync tries to wake up a blocked async operation.
167-
// It returns true if one was woken, false otherwise.
168-
func (as *asyncTestState) wakeAsync() bool {
169-
as.mu.Lock()
170-
var woken *blockedAsync
171-
for w := range as.blocked {
172-
if w.until() {
173-
woken = w
174-
delete(as.blocked, w)
175-
break
176-
}
177-
}
178-
as.mu.Unlock()
179-
if woken == nil {
180-
return false
181-
}
182-
close(woken.donec)
183-
<-as.notify // must not hold as.mu while blocked here
184-
return true
185-
}

0 commit comments

Comments
 (0)