@@ -2,7 +2,9 @@ package cli
22
33import (
44"context"
5+ "fmt"
56"net/url"
7+ "sync"
68"testing"
79"time"
810
@@ -12,6 +14,7 @@ import (
1214
1315"cdr.dev/slog"
1416"cdr.dev/slog/sloggers/slogtest"
17+ "github.com/coder/quartz"
1518
1619"github.com/coder/coder/v2/codersdk"
1720"github.com/coder/coder/v2/testutil"
@@ -68,7 +71,7 @@ func TestCloserStack_Mainline(t *testing.T) {
6871t .Parallel ()
6972ctx := testutil .Context (t ,testutil .WaitShort )
7073logger := slogtest .Make (t ,nil ).Leveled (slog .LevelDebug )
71- uut := newCloserStack (ctx ,logger )
74+ uut := newCloserStack (ctx ,logger , quartz . NewMock ( t ) )
7275closes := new ([]* fakeCloser )
7376fc0 := & fakeCloser {closes :closes }
7477fc1 := & fakeCloser {closes :closes }
@@ -84,13 +87,27 @@ func TestCloserStack_Mainline(t *testing.T) {
8487require .Equal (t , []* fakeCloser {fc1 ,fc0 },* closes )
8588}
8689
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+
87104func TestCloserStack_Context (t * testing.T ) {
88105t .Parallel ()
89106ctx := testutil .Context (t ,testutil .WaitShort )
90107ctx ,cancel := context .WithCancel (ctx )
91108defer cancel ()
92109logger := slogtest .Make (t ,nil ).Leveled (slog .LevelDebug )
93- uut := newCloserStack (ctx ,logger )
110+ uut := newCloserStack (ctx ,logger , quartz . NewMock ( t ) )
94111closes := new ([]* fakeCloser )
95112fc0 := & fakeCloser {closes :closes }
96113fc1 := & fakeCloser {closes :closes }
@@ -111,7 +128,7 @@ func TestCloserStack_PushAfterClose(t *testing.T) {
111128t .Parallel ()
112129ctx := testutil .Context (t ,testutil .WaitShort )
113130logger := slogtest .Make (t ,& slogtest.Options {IgnoreErrors :true }).Leveled (slog .LevelDebug )
114- uut := newCloserStack (ctx ,logger )
131+ uut := newCloserStack (ctx ,logger , quartz . NewMock ( t ) )
115132closes := new ([]* fakeCloser )
116133fc0 := & fakeCloser {closes :closes }
117134fc1 := & fakeCloser {closes :closes }
@@ -134,13 +151,9 @@ func TestCloserStack_CloseAfterContext(t *testing.T) {
134151ctx ,cancel := context .WithCancel (testCtx )
135152defer cancel ()
136153logger := 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 ()
144157err := uut .push ("async" ,ac )
145158require .NoError (t ,err )
146159cancel ()
@@ -160,11 +173,53 @@ func TestCloserStack_CloseAfterContext(t *testing.T) {
160173t .Fatal ("closed before stack was finished" )
161174}
162175
163- // complete the asyncCloser
164- close (ac .complete )
176+ ac .complete ()
165177testutil .RequireRecvCtx (testCtx ,t ,closed )
166178}
167179
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+
168223type fakeCloser struct {
169224closes * []* fakeCloser
170225err error
@@ -176,10 +231,11 @@ func (c *fakeCloser) Close() error {
176231}
177232
178233type 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
183239}
184240
185241func (c * asyncCloser )Close ()error {
@@ -188,7 +244,20 @@ func (c *asyncCloser) Close() error {
188244case <- c .ctx .Done ():
189245c .t .Error ("timed out" )
190246return c .ctx .Err ()
191- case <- c .complete :
247+ case <- c .isComplete :
192248return nil
193249}
194250}
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+ }