aboutsummaryrefslogtreecommitdiffstats
path: root/logging/test
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--logging/test/handler.go112
1 files changed, 112 insertions, 0 deletions
diff --git a/logging/test/handler.go b/logging/test/handler.go
new file mode 100644
index 0000000..d1f5d40
--- /dev/null
+++ b/logging/test/handler.go
@@ -0,0 +1,112 @@
+package test
+
+// Package test provides mocks and helpers for testing code that uses slog logging.
+//
+// It includes MockHandler, a thread-safe slog.Handler implementation for capturing log records in tests.
+
+import (
+ "context"
+ "log/slog"
+ "slices"
+ "sync"
+ "testing"
+)
+
+type logRecorder struct {
+ mu sync.Mutex
+ records []slog.Record
+}
+
+func (h *logRecorder) Append(r slog.Record) {
+ h.mu.Lock()
+ defer h.mu.Unlock()
+
+ h.records = append(h.records, r.Clone())
+}
+
+func (h *logRecorder) Records() []slog.Record {
+ h.mu.Lock()
+ defer h.mu.Unlock()
+ return slices.Clone(h.records)
+}
+
+// MockHandler is a slog.Handler that records log records for testing.
+//
+// It supports attribute and group chaining like slog's built-in handlers.
+// Use NewMockLogHandler to construct one, then pass it to slog.New in your tests.
+//
+// All recorded logs can be retrieved with the Records method.
+type MockHandler struct {
+ recorder *logRecorder
+ parent *MockHandler
+ group string
+ attrs []slog.Attr
+}
+
+var _ slog.Handler = &MockHandler{}
+
+// NewMockLogHandler creates a new MockHandler for use in tests.
+func NewMockLogHandler(tb testing.TB) *MockHandler {
+ tb.Helper()
+
+ return &MockHandler{
+ recorder: &logRecorder{},
+ }
+}
+
+func (h *MockHandler) Enabled(ctx context.Context, level slog.Level) bool {
+ return true // Capture all logs
+}
+
+func (h *MockHandler) Handle(ctx context.Context, r slog.Record) error {
+ if h.parent == nil {
+ r.Clone()
+ h.recorder.Append(r)
+ return nil
+ }
+
+ newRecord := slog.NewRecord(r.Time, r.Level, r.Message, r.PC)
+
+ attrs := slices.Clone(h.attrs)
+ r.Attrs(func(attr slog.Attr) bool {
+ attrs = append(attrs, attr)
+ return true
+ })
+
+ if h.group != "" {
+ newRecord.AddAttrs(slog.Attr{
+ Key: h.group,
+ Value: slog.GroupValue(attrs...),
+ })
+ } else {
+ newRecord.AddAttrs(attrs...)
+ }
+
+ return h.parent.Handle(ctx, newRecord)
+}
+
+func (h *MockHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
+ return &MockHandler{
+ recorder: h.recorder,
+ parent: h,
+ attrs: attrs,
+ }
+}
+
+func (h *MockHandler) WithGroup(name string) slog.Handler {
+ if name == "" {
+ return h
+ }
+ return &MockHandler{
+ recorder: h.recorder,
+ parent: h,
+ group: name,
+ }
+}
+
+// Records returns a copy of all slog.Records captured by this handler so far.
+//
+// It is safe to call from multiple goroutines.
+func (h *MockHandler) Records() []slog.Record {
+ return h.recorder.Records()
+}