@@ -2,7 +2,9 @@ package cli
2
2
3
3
import (
4
4
"context"
5
+ "fmt"
5
6
"net/url"
7
+ "sync"
6
8
"testing"
7
9
"time"
8
10
@@ -12,6 +14,7 @@ import (
12
14
13
15
"cdr.dev/slog"
14
16
"cdr.dev/slog/sloggers/slogtest"
17
+ "github.com/coder/quartz"
15
18
16
19
"github.com/coder/coder/v2/codersdk"
17
20
"github.com/coder/coder/v2/testutil"
@@ -68,7 +71,7 @@ func TestCloserStack_Mainline(t *testing.T) {
68
71
t .Parallel ()
69
72
ctx := testutil .Context (t , testutil .WaitShort )
70
73
logger := slogtest .Make (t , nil ).Leveled (slog .LevelDebug )
71
- uut := newCloserStack (ctx , logger )
74
+ uut := newCloserStack (ctx , logger , quartz . NewMock ( t ) )
72
75
closes := new ([]* fakeCloser )
73
76
fc0 := & fakeCloser {closes : closes }
74
77
fc1 := & fakeCloser {closes : closes }
@@ -90,7 +93,7 @@ func TestCloserStack_Context(t *testing.T) {
90
93
ctx , cancel := context .WithCancel (ctx )
91
94
defer cancel ()
92
95
logger := slogtest .Make (t , nil ).Leveled (slog .LevelDebug )
93
- uut := newCloserStack (ctx , logger )
96
+ uut := newCloserStack (ctx , logger , quartz . NewMock ( t ) )
94
97
closes := new ([]* fakeCloser )
95
98
fc0 := & fakeCloser {closes : closes }
96
99
fc1 := & fakeCloser {closes : closes }
@@ -111,7 +114,7 @@ func TestCloserStack_PushAfterClose(t *testing.T) {
111
114
t .Parallel ()
112
115
ctx := testutil .Context (t , testutil .WaitShort )
113
116
logger := slogtest .Make (t , & slogtest.Options {IgnoreErrors : true }).Leveled (slog .LevelDebug )
114
- uut := newCloserStack (ctx , logger )
117
+ uut := newCloserStack (ctx , logger , quartz . NewMock ( t ) )
115
118
closes := new ([]* fakeCloser )
116
119
fc0 := & fakeCloser {closes : closes }
117
120
fc1 := & fakeCloser {closes : closes }
@@ -134,13 +137,9 @@ func TestCloserStack_CloseAfterContext(t *testing.T) {
134
137
ctx , cancel := context .WithCancel (testCtx )
135
138
defer cancel ()
136
139
logger := slogtest .Make (t , & slogtest.Options {IgnoreErrors : true }).Leveled (slog .LevelDebug )
137
- uut := newCloserStack (ctx , logger )
138
- ac := & asyncCloser {
139
- t : t ,
140
- ctx : testCtx ,
141
- complete : make (chan struct {}),
142
- started : make (chan struct {}),
143
- }
140
+ uut := newCloserStack (ctx , logger , quartz .NewMock (t ))
141
+ ac := newAsyncCloser (testCtx , t )
142
+ defer ac .complete ()
144
143
err := uut .push ("async" , ac )
145
144
require .NoError (t , err )
146
145
cancel ()
@@ -160,11 +159,53 @@ func TestCloserStack_CloseAfterContext(t *testing.T) {
160
159
t .Fatal ("closed before stack was finished" )
161
160
}
162
161
163
- // complete the asyncCloser
164
- close (ac .complete )
162
+ ac .complete ()
165
163
testutil .RequireRecvCtx (testCtx , t , closed )
166
164
}
167
165
166
+ func TestCloserStack_Timeout (t * testing.T ) {
167
+ t .Parallel ()
168
+ ctx := testutil .Context (t , testutil .WaitShort )
169
+ logger := slogtest .Make (t , & slogtest.Options {IgnoreErrors : true }).Leveled (slog .LevelDebug )
170
+ mClock := quartz .NewMock (t )
171
+ trap := mClock .Trap ().TickerFunc ("closerStack" )
172
+ defer trap .Close ()
173
+ uut := newCloserStack (ctx , logger , mClock )
174
+ var ac [3 ]* asyncCloser
175
+ for i := range ac {
176
+ ac [i ] = newAsyncCloser (ctx , t )
177
+ err := uut .push (fmt .Sprintf ("async %d" , i ), ac [i ])
178
+ require .NoError (t , err )
179
+ }
180
+ defer func () {
181
+ for _ , a := range ac {
182
+ a .complete ()
183
+ }
184
+ }()
185
+
186
+ closed := make (chan struct {})
187
+ go func () {
188
+ defer close (closed )
189
+ uut .close (nil )
190
+ }()
191
+ trap .MustWait (ctx ).Release ()
192
+ // top starts right away, but it hangs
193
+ testutil .RequireRecvCtx (ctx , t , ac [2 ].started )
194
+ // timer pops and we start the middle one
195
+ mClock .Advance (gracefulShutdownTimeout ).MustWait (ctx )
196
+ testutil .RequireRecvCtx (ctx , t , ac [1 ].started )
197
+
198
+ // middle one finishes
199
+ ac [1 ].complete ()
200
+ // bottom starts, but also hangs
201
+ testutil .RequireRecvCtx (ctx , t , ac [0 ].started )
202
+
203
+ // timer has to pop twice to time out.
204
+ mClock .Advance (gracefulShutdownTimeout ).MustWait (ctx )
205
+ mClock .Advance (gracefulShutdownTimeout ).MustWait (ctx )
206
+ testutil .RequireRecvCtx (ctx , t , closed )
207
+ }
208
+
168
209
type fakeCloser struct {
169
210
closes * []* fakeCloser
170
211
err error
@@ -176,10 +217,11 @@ func (c *fakeCloser) Close() error {
176
217
}
177
218
178
219
type asyncCloser struct {
179
- t * testing.T
180
- ctx context.Context
181
- started chan struct {}
182
- complete chan struct {}
220
+ t * testing.T
221
+ ctx context.Context
222
+ started chan struct {}
223
+ isComplete chan struct {}
224
+ comepleteOnce sync.Once
183
225
}
184
226
185
227
func (c * asyncCloser ) Close () error {
@@ -188,7 +230,20 @@ func (c *asyncCloser) Close() error {
188
230
case <- c .ctx .Done ():
189
231
c .t .Error ("timed out" )
190
232
return c .ctx .Err ()
191
- case <- c .complete :
233
+ case <- c .isComplete :
192
234
return nil
193
235
}
194
236
}
237
+
238
+ func (c * asyncCloser ) complete () {
239
+ c .comepleteOnce .Do (func () { close (c .isComplete ) })
240
+ }
241
+
242
+ func newAsyncCloser (ctx context.Context , t * testing.T ) * asyncCloser {
243
+ return & asyncCloser {
244
+ t : t ,
245
+ ctx : ctx ,
246
+ isComplete : make (chan struct {}),
247
+ started : make (chan struct {}),
248
+ }
249
+ }
0 commit comments