@@ -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 }
@@ -84,13 +87,27 @@ func TestCloserStack_Mainline(t *testing.T) {
84
87
require .Equal (t , []* fakeCloser {fc1 , fc0 }, * closes )
85
88
}
86
89
90
+ func TestCloserStack_Empty (t * testing.T ) {
91
+ t .Parallel ()
92
+ ctx := testutil .Context (t , testutil .WaitShort )
93
+ logger := slogtest .Make (t , nil ).Leveled (slog .LevelDebug )
94
+ uut := newCloserStack (ctx , logger , quartz .NewMock (t ))
95
+
96
+ closed := make (chan struct {})
97
+ go func () {
98
+ defer close (closed )
99
+ uut .close (nil )
100
+ }()
101
+ testutil .RequireRecvCtx (ctx , t , closed )
102
+ }
103
+
87
104
func TestCloserStack_Context (t * testing.T ) {
88
105
t .Parallel ()
89
106
ctx := testutil .Context (t , testutil .WaitShort )
90
107
ctx , cancel := context .WithCancel (ctx )
91
108
defer cancel ()
92
109
logger := slogtest .Make (t , nil ).Leveled (slog .LevelDebug )
93
- uut := newCloserStack (ctx , logger )
110
+ uut := newCloserStack (ctx , logger , quartz . NewMock ( t ) )
94
111
closes := new ([]* fakeCloser )
95
112
fc0 := & fakeCloser {closes : closes }
96
113
fc1 := & fakeCloser {closes : closes }
@@ -111,7 +128,7 @@ func TestCloserStack_PushAfterClose(t *testing.T) {
111
128
t .Parallel ()
112
129
ctx := testutil .Context (t , testutil .WaitShort )
113
130
logger := slogtest .Make (t , & slogtest.Options {IgnoreErrors : true }).Leveled (slog .LevelDebug )
114
- uut := newCloserStack (ctx , logger )
131
+ uut := newCloserStack (ctx , logger , quartz . NewMock ( t ) )
115
132
closes := new ([]* fakeCloser )
116
133
fc0 := & fakeCloser {closes : closes }
117
134
fc1 := & fakeCloser {closes : closes }
@@ -134,13 +151,9 @@ func TestCloserStack_CloseAfterContext(t *testing.T) {
134
151
ctx , cancel := context .WithCancel (testCtx )
135
152
defer cancel ()
136
153
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
- }
154
+ uut := newCloserStack (ctx , logger , quartz .NewMock (t ))
155
+ ac := newAsyncCloser (testCtx , t )
156
+ defer ac .complete ()
144
157
err := uut .push ("async" , ac )
145
158
require .NoError (t , err )
146
159
cancel ()
@@ -160,11 +173,53 @@ func TestCloserStack_CloseAfterContext(t *testing.T) {
160
173
t .Fatal ("closed before stack was finished" )
161
174
}
162
175
163
- // complete the asyncCloser
164
- close (ac .complete )
176
+ ac .complete ()
165
177
testutil .RequireRecvCtx (testCtx , t , closed )
166
178
}
167
179
180
+ func TestCloserStack_Timeout (t * testing.T ) {
181
+ t .Parallel ()
182
+ ctx := testutil .Context (t , testutil .WaitShort )
183
+ logger := slogtest .Make (t , & slogtest.Options {IgnoreErrors : true }).Leveled (slog .LevelDebug )
184
+ mClock := quartz .NewMock (t )
185
+ trap := mClock .Trap ().TickerFunc ("closerStack" )
186
+ defer trap .Close ()
187
+ uut := newCloserStack (ctx , logger , mClock )
188
+ var ac [3 ]* asyncCloser
189
+ for i := range ac {
190
+ ac [i ] = newAsyncCloser (ctx , t )
191
+ err := uut .push (fmt .Sprintf ("async %d" , i ), ac [i ])
192
+ require .NoError (t , err )
193
+ }
194
+ defer func () {
195
+ for _ , a := range ac {
196
+ a .complete ()
197
+ }
198
+ }()
199
+
200
+ closed := make (chan struct {})
201
+ go func () {
202
+ defer close (closed )
203
+ uut .close (nil )
204
+ }()
205
+ trap .MustWait (ctx ).Release ()
206
+ // top starts right away, but it hangs
207
+ testutil .RequireRecvCtx (ctx , t , ac [2 ].started )
208
+ // timer pops and we start the middle one
209
+ mClock .Advance (gracefulShutdownTimeout ).MustWait (ctx )
210
+ testutil .RequireRecvCtx (ctx , t , ac [1 ].started )
211
+
212
+ // middle one finishes
213
+ ac [1 ].complete ()
214
+ // bottom starts, but also hangs
215
+ testutil .RequireRecvCtx (ctx , t , ac [0 ].started )
216
+
217
+ // timer has to pop twice to time out.
218
+ mClock .Advance (gracefulShutdownTimeout ).MustWait (ctx )
219
+ mClock .Advance (gracefulShutdownTimeout ).MustWait (ctx )
220
+ testutil .RequireRecvCtx (ctx , t , closed )
221
+ }
222
+
168
223
type fakeCloser struct {
169
224
closes * []* fakeCloser
170
225
err error
@@ -176,10 +231,11 @@ func (c *fakeCloser) Close() error {
176
231
}
177
232
178
233
type asyncCloser struct {
179
- t * testing.T
180
- ctx context.Context
181
- started chan struct {}
182
- complete chan struct {}
234
+ t * testing.T
235
+ ctx context.Context
236
+ started chan struct {}
237
+ isComplete chan struct {}
238
+ comepleteOnce sync.Once
183
239
}
184
240
185
241
func (c * asyncCloser ) Close () error {
@@ -188,7 +244,20 @@ func (c *asyncCloser) Close() error {
188
244
case <- c .ctx .Done ():
189
245
c .t .Error ("timed out" )
190
246
return c .ctx .Err ()
191
- case <- c .complete :
247
+ case <- c .isComplete :
192
248
return nil
193
249
}
194
250
}
251
+
252
+ func (c * asyncCloser ) complete () {
253
+ c .comepleteOnce .Do (func () { close (c .isComplete ) })
254
+ }
255
+
256
+ func newAsyncCloser (ctx context.Context , t * testing.T ) * asyncCloser {
257
+ return & asyncCloser {
258
+ t : t ,
259
+ ctx : ctx ,
260
+ isComplete : make (chan struct {}),
261
+ started : make (chan struct {}),
262
+ }
263
+ }
0 commit comments