mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
15c958fea2
`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)
89 lines
2.0 KiB
Go
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
|
|
}
|