1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
|
package cli
import (
"context"
"log/slog"
"net"
"net/http"
"sync"
"testing"
"time"
)
// recordHandler captures log records for assertions.
type recordHandler struct {
mu sync.Mutex
records []slog.Record
}
func (h *recordHandler) Enabled(_ context.Context, _ slog.Level) bool { return true }
func (h *recordHandler) Handle(_ context.Context, r slog.Record) error {
h.mu.Lock()
h.records = append(h.records, r)
h.mu.Unlock()
return nil
}
func (h *recordHandler) WithAttrs(_ []slog.Attr) slog.Handler { return h }
func (h *recordHandler) WithGroup(_ string) slog.Handler { return h }
func (h *recordHandler) hasWarn(msg string) bool {
h.mu.Lock()
defer h.mu.Unlock()
for _, r := range h.records {
if r.Level == slog.LevelWarn && r.Message == msg {
return true
}
}
return false
}
// TestServe_ShutdownError_IsLogged verifies that a shutdown timeout error is
// logged as a warning rather than silently dropped.
func TestServe_ShutdownError_IsLogged(t *testing.T) {
// Start a real listener so we have an address.
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatalf("listen: %v", err)
}
// Handler that hangs so the active connection prevents clean shutdown.
hang := make(chan struct{})
mux := http.NewServeMux()
mux.HandleFunc("/hang", func(w http.ResponseWriter, r *http.Request) {
<-hang
})
srv := &http.Server{Handler: mux}
// Serve in background.
go srv.Serve(ln) //nolint:errcheck
// Open a connection and start a hanging request so the server has an
// active connection when we call Shutdown.
addr := ln.Addr().String()
connReady := make(chan struct{})
go func() {
req, _ := http.NewRequest(http.MethodGet, "http://"+addr+"/hang", nil)
close(connReady)
http.DefaultClient.Do(req) //nolint:errcheck
}()
<-connReady
// Give the goroutine a moment to establish the request.
time.Sleep(20 * time.Millisecond)
// Shutdown with an already-expired deadline so it times out immediately.
expiredCtx, cancel := context.WithDeadline(context.Background(), time.Now().Add(-1*time.Second))
defer cancel()
h := &recordHandler{}
logger := slog.New(h)
// This is the exact logic from serve.go's shutdown goroutine.
if err := srv.Shutdown(expiredCtx); err != nil {
logger.Warn("shutdown error", "err", err)
}
// Unblock the hanging handler.
close(hang)
if !h.hasWarn("shutdown error") {
t.Error("expected shutdown error to be logged as Warn, but it was not")
}
}
|