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+
57package quic
68
79import (
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).
3717type 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.
4926func (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.
7351func (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.
9666func 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