-
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathhypercache_logging_test.go
More file actions
172 lines (139 loc) · 5.38 KB
/
hypercache_logging_test.go
File metadata and controls
172 lines (139 loc) · 5.38 KB
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
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
package hypercache_test
import (
"bytes"
"context"
"log/slog"
"strings"
"testing"
"time"
"github.com/goccy/go-json"
"github.com/hyp3rd/hypercache"
"github.com/hyp3rd/hypercache/internal/constants"
"github.com/hyp3rd/hypercache/pkg/backend"
)
// TestLogging_EvictionLoopAnnouncesStart pins the structured-log
// contract operators rely on: when the eviction loop is configured
// (evictionInterval > 0), starting the cache emits an "eviction loop
// starting" record with the interval, max_per_tick, and algorithm
// fields. Without this, a silent cache failed silently — the symptom
// the WithLogger work was added to fix.
func TestLogging_EvictionLoopAnnouncesStart(t *testing.T) {
t.Parallel()
buf := &bytes.Buffer{}
logger := slog.New(slog.NewJSONHandler(buf, &slog.HandlerOptions{Level: slog.LevelDebug}))
cfg, err := hypercache.NewConfig[backend.InMemory](constants.InMemoryBackend)
if err != nil {
t.Fatalf("NewConfig: %v", err)
}
cfg.HyperCacheOptions = []hypercache.Option[backend.InMemory]{
hypercache.WithEvictionInterval[backend.InMemory](50 * time.Millisecond),
hypercache.WithEvictionAlgorithm[backend.InMemory]("lru"),
hypercache.WithLogger[backend.InMemory](logger),
}
cfg.InMemoryOptions = []backend.Option[backend.InMemory]{
backend.WithCapacity[backend.InMemory](10),
}
hc, err := hypercache.New(context.Background(), hypercache.GetDefaultManager(), cfg)
if err != nil {
t.Fatalf("New: %v", err)
}
t.Cleanup(func() { _ = hc.Stop(context.Background()) })
// Loop startup log is emitted synchronously from startEvictionRoutine,
// so we don't need to wait for a tick.
rec := firstRecordMatching(t, buf, "eviction loop starting")
// slog's JSON handler encodes time.Duration as nanoseconds (float64).
wantNs := float64((50 * time.Millisecond).Nanoseconds())
got, ok := rec["interval"].(float64)
if !ok {
t.Errorf("interval: missing or wrong type, got %v (%T)", rec["interval"], rec["interval"])
} else if got != wantNs {
t.Errorf("interval: want %v ns, got %v", wantNs, got)
}
if rec["algorithm"] != "lru" {
t.Errorf("algorithm: want lru, got %v", rec["algorithm"])
}
}
// TestLogging_ExpirationLoopAnnouncesStart mirrors the eviction
// assertion for the expiration loop. Same operational contract,
// same regression guard.
func TestLogging_ExpirationLoopAnnouncesStart(t *testing.T) {
t.Parallel()
buf := &bytes.Buffer{}
logger := slog.New(slog.NewJSONHandler(buf, &slog.HandlerOptions{Level: slog.LevelDebug}))
cfg, err := hypercache.NewConfig[backend.InMemory](constants.InMemoryBackend)
if err != nil {
t.Fatalf("NewConfig: %v", err)
}
cfg.HyperCacheOptions = []hypercache.Option[backend.InMemory]{
hypercache.WithExpirationInterval[backend.InMemory](75 * time.Millisecond),
hypercache.WithLogger[backend.InMemory](logger),
}
cfg.InMemoryOptions = []backend.Option[backend.InMemory]{
backend.WithCapacity[backend.InMemory](10),
}
hc, err := hypercache.New(context.Background(), hypercache.GetDefaultManager(), cfg)
if err != nil {
t.Fatalf("New: %v", err)
}
t.Cleanup(func() { _ = hc.Stop(context.Background()) })
rec := firstRecordMatching(t, buf, "expiration loop starting")
wantNs := float64((75 * time.Millisecond).Nanoseconds())
got, ok := rec["interval"].(float64)
if !ok {
t.Errorf("interval: missing or wrong type, got %v (%T)", rec["interval"], rec["interval"])
} else if got != wantNs {
t.Errorf("interval: want %v ns, got %v", wantNs, got)
}
}
// TestLogging_NilLoggerResetsToDiscard documents the WithLogger(nil)
// contract: passing nil resets to the default discard handler instead
// of panicking later. Operators may unset logging at runtime via the
// `nil` shape; the cache should keep working silently.
func TestLogging_NilLoggerResetsToDiscard(t *testing.T) {
t.Parallel()
cfg, err := hypercache.NewConfig[backend.InMemory](constants.InMemoryBackend)
if err != nil {
t.Fatalf("NewConfig: %v", err)
}
cfg.HyperCacheOptions = []hypercache.Option[backend.InMemory]{
hypercache.WithLogger[backend.InMemory](nil),
}
cfg.InMemoryOptions = []backend.Option[backend.InMemory]{
backend.WithCapacity[backend.InMemory](10),
}
hc, err := hypercache.New(context.Background(), hypercache.GetDefaultManager(), cfg)
if err != nil {
t.Fatalf("New: %v", err)
}
t.Cleanup(func() { _ = hc.Stop(context.Background()) })
// If WithLogger(nil) had been wired as an unguarded assignment, the
// next operation would NPE on logger.Info. The fact that we reach
// this line and the cache responds is the assertion.
err = hc.Set(context.Background(), "k", "v", time.Minute)
if err != nil {
t.Fatalf("Set with nil logger should succeed; got: %v", err)
}
}
// firstRecordMatching scans the JSON log stream for the first record
// whose `msg` matches the prefix, returning it as a generic map. Fails
// the test fatally if no such record exists — the assertion is "this
// log line MUST appear", not "may appear".
func firstRecordMatching(t *testing.T, buf *bytes.Buffer, msgPrefix string) map[string]any {
t.Helper()
for line := range strings.SplitSeq(buf.String(), "\n") {
if line == "" {
continue
}
var rec map[string]any
err := json.Unmarshal([]byte(line), &rec)
if err != nil {
t.Fatalf("malformed log line: %q (%v)", line, err)
}
msg, ok := rec["msg"].(string)
if ok && strings.HasPrefix(msg, msgPrefix) {
return rec
}
}
t.Fatalf("no log record with msg prefix %q in:\n%s", msgPrefix, buf.String())
return nil
}