Files
coder/testutil/fake_sink.go
T
Cian Johnston 15c958fea2 fix(testutil): ensure FakeSink does not swallow logs (#25185)
`FakeSink` was silently capturing log entries without forwarding them to
`testing.TB.Log`. This made debugging test failures harder because logs
were invisible in `go test -v` output.

Store `testing.TB` in `FakeSink` and call `t.Log` on each entry, guarded
by a check to avoid logging after the test has finished.

Split out from #25012.

> 🤖 Generated with [Coder Agents](https://coder.com)
2026-05-14 16:51:44 +01:00

89 lines
2.0 KiB
Go

package testutil
import (
"context"
"sync"
"testing"
"cdr.dev/slog/v3"
)
// FakeSink is a thread-safe slog.Sink that captures log entries so
// tests can assert on what was logged. It requires a testing.TB
// as it is only meant for use in tests.
type FakeSink struct {
t testing.TB
mu sync.RWMutex
entries []slog.SinkEntry
tDone bool
}
// NewFakeSink returns a FakeSink ready for use.
func NewFakeSink(t testing.TB) *FakeSink {
fs := &FakeSink{t: t}
t.Cleanup(func() {
fs.mu.Lock()
fs.tDone = true
fs.mu.Unlock()
})
return fs
}
// LogEntry implements slog.Sink. It appends the entry to the
// internal slice.
func (s *FakeSink) LogEntry(_ context.Context, e slog.SinkEntry) {
s.mu.Lock()
s.entries = append(s.entries, e)
shouldLog := !s.tDone
s.mu.Unlock()
if shouldLog {
s.t.Log(e.Message, e.Fields)
}
}
// Sync implements slog.Sink.
func (*FakeSink) Sync() {}
// Entries returns a copy of the captured entries. If filters are
// provided, only entries matching ALL filters are returned. This
// lets callers compose simple predicates instead of needing
// dedicated methods for each field.
func (s *FakeSink) Entries(filters ...func(slog.SinkEntry) bool) []slog.SinkEntry {
s.mu.RLock()
cpy := make([]slog.SinkEntry, len(s.entries))
copy(cpy, s.entries)
s.mu.RUnlock()
filtered := make([]slog.SinkEntry, 0)
for _, e := range cpy {
if !matchAll(e, filters) {
continue
}
filtered = append(filtered, e)
}
return filtered
}
// Logger returns a slog.Logger backed by this sink at the given
// level. If no level is provided it defaults to LevelDebug, which
// captures everything. If more than one level is provided, the
// first one wins.
func (s *FakeSink) Logger(level ...slog.Level) slog.Logger {
l := slog.LevelDebug
if len(level) > 0 {
l = level[0]
}
return slog.Make(s).Leveled(l)
}
func matchAll(e slog.SinkEntry, filters []func(slog.SinkEntry) bool) bool {
for _, f := range filters {
if f == nil {
continue
}
if !f(e) {
return false
}
}
return true
}