From 3734877b19762384d91ba36429c2b8ed3986a871 Mon Sep 17 00:00:00 2001 From: Bastien Gysler Date: Wed, 25 Jun 2025 09:23:37 +0200 Subject: [PATCH] feat: custom mock logger (#21) Adds a `WithLogger` method to the Mock clock that allows customising the logger used for debug output, instead of always using `testing.TB`. The main motivation is that Quartz can be verbose. --- mock.go | 43 +++++++++++++++++++++++++++++++++++-------- mock_test.go | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 8 deletions(-) diff --git a/mock.go b/mock.go index 1b0c48c..bd65ddb 100644 --- a/mock.go +++ b/mock.go @@ -14,6 +14,7 @@ import ( // during a test, triggering any timers or tickers automatically. type Mock struct { tb testing.TB + logger Logger mu sync.Mutex testOver bool @@ -199,7 +200,7 @@ func (m *Mock) matchCallLocked(c *apiCall) { } } if !m.testOver { - m.tb.Logf("Mock Clock - %s call, matched %d traps", c, len(traps)) + m.logger.Logf("Mock Clock - %s call, matched %d traps", c, len(traps)) } if len(traps) == 0 { return @@ -265,7 +266,7 @@ func (m *Mock) Advance(d time.Duration) AdvanceWaiter { w := AdvanceWaiter{tb: m.tb, ch: make(chan struct{})} m.mu.Lock() if !m.testOver { - m.tb.Logf("Mock Clock - Advance(%s)", d) + m.logger.Logf("Mock Clock - Advance(%s)", d) } fin := m.cur.Add(d) // nextTime.IsZero implies no events scheduled. @@ -315,7 +316,7 @@ func (m *Mock) Set(t time.Time) AdvanceWaiter { w := AdvanceWaiter{tb: m.tb, ch: make(chan struct{})} m.mu.Lock() if !m.testOver { - m.tb.Logf("Mock Clock - Set(%s)", t) + m.logger.Logf("Mock Clock - Set(%s)", t) } if t.Before(m.cur) { defer close(w.ch) @@ -354,7 +355,7 @@ func (m *Mock) Set(t time.Time) AdvanceWaiter { func (m *Mock) AdvanceNext() (time.Duration, AdvanceWaiter) { m.mu.Lock() if !m.testOver { - m.tb.Logf("Mock Clock - AdvanceNext()") + m.logger.Logf("Mock Clock - AdvanceNext()") } m.tb.Helper() w := AdvanceWaiter{tb: m.tb, ch: make(chan struct{})} @@ -445,7 +446,7 @@ func (m *Mock) newTrap(fn clockFunction, tags []string) *Trap { m.mu.Lock() defer m.mu.Unlock() if !m.testOver { - m.tb.Logf("Mock Clock - Trap %s(..., %v)", fn, tags) + m.logger.Logf("Mock Clock - Trap %s(..., %v)", fn, tags) } tr := &Trap{ fn: fn, @@ -458,6 +459,18 @@ func (m *Mock) newTrap(fn clockFunction, tags []string) *Trap { return tr } +// WithLogger replaces the default testing logger with a custom one. +// +// This can be used to discard log messages with: +// +// quartz.NewMock(t).WithLogger(quartz.NoOpLogger) +func (m *Mock) WithLogger(l Logger) *Mock { + m.mu.Lock() + defer m.mu.Unlock() + m.logger = l + return m +} + // NewMock creates a new Mock with the time set to midnight UTC on Jan 1, 2024. // You may re-set the time earlier than this, but only before timers or tickers // are created. @@ -467,14 +480,15 @@ func NewMock(tb testing.TB) *Mock { panic(err) } m := &Mock{ - tb: tb, - cur: cur, + tb: tb, + logger: tb, + cur: cur, } tb.Cleanup(func() { m.mu.Lock() defer m.mu.Unlock() m.testOver = true - tb.Logf("Mock Clock - test cleanup; will no longer log clock events") + m.logger.Logf("Mock Clock - test cleanup; will no longer log clock events") }) return m } @@ -806,3 +820,16 @@ func (t *Trap) MustWait(ctx context.Context) *Call { } return c } + +type Logger interface { + Log(args ...any) + Logf(format string, args ...any) +} + +// NoOpLogger is a Logger that discards all log messages. +var NoOpLogger Logger = noOpLogger{} + +type noOpLogger struct{} + +func (noOpLogger) Log(args ...any) {} +func (noOpLogger) Logf(format string, args ...any) {} diff --git a/mock_test.go b/mock_test.go index 5663762..ae3d14c 100644 --- a/mock_test.go +++ b/mock_test.go @@ -3,6 +3,7 @@ package quartz_test import ( "context" "errors" + "fmt" "testing" "time" @@ -407,6 +408,39 @@ func Test_UnreleasedCalls(t *testing.T) { }) } +func Test_WithLogger(t *testing.T) { + t.Parallel() + + tl := &testLogger{} + mClock := quartz.NewMock(t).WithLogger(tl) + mClock.Now("test", "Test_WithLogger") + if len(tl.calls) != 1 { + t.Fatalf("expected 1 call, got %d", len(tl.calls)) + } + expectLogLine := "Mock Clock - Now([test Test_WithLogger]) call, matched 0 traps" + if tl.calls[0] != expectLogLine { + t.Fatalf("expected log line %q, got %q", expectLogLine, tl.calls[0]) + } + + mClock.NewTimer(time.Second, "timer") + if len(tl.calls) != 2 { + t.Fatalf("expected 2 calls, got %d", len(tl.calls)) + } + expectLogLine = "Mock Clock - NewTimer(1s, [timer]) call, matched 0 traps" + if tl.calls[1] != expectLogLine { + t.Fatalf("expected log line %q, got %q", expectLogLine, tl.calls[1]) + } + + mClock.Advance(500 * time.Millisecond) + if len(tl.calls) != 3 { + t.Fatalf("expected 3 calls, got %d", len(tl.calls)) + } + expectLogLine = "Mock Clock - Advance(500ms)" + if tl.calls[2] != expectLogLine { + t.Fatalf("expected log line %q, got %q", expectLogLine, tl.calls[2]) + } +} + type captureFailTB struct { failed bool testing.TB @@ -455,3 +489,15 @@ func tRunFail(t testing.TB, f func(t testing.TB)) { t.Fatal("want test to fail") } } + +type testLogger struct { + calls []string +} + +func (l *testLogger) Log(args ...any) { + l.calls = append(l.calls, fmt.Sprint(args...)) +} + +func (l *testLogger) Logf(format string, args ...any) { + l.calls = append(l.calls, fmt.Sprintf(format, args...)) +}