package starlog import ( "bytes" "context" "strings" "sync/atomic" "testing" "time" ) func waitObserverCount(t *testing.T, observer *Observer, want int, timeout time.Duration) { t.Helper() deadline := time.Now().Add(timeout) for time.Now().Before(deadline) { if observer != nil && observer.Count() >= want { return } time.Sleep(5 * time.Millisecond) } got := 0 if observer != nil { got = observer.Count() } t.Fatalf("observer count timeout, want >= %d got %d", want, got) } func waitObserverCondition(t *testing.T, timeout time.Duration, cond func() bool, reason string) { t.Helper() deadline := time.Now().Add(timeout) for time.Now().Before(deadline) { if cond() { return } time.Sleep(5 * time.Millisecond) } t.Fatalf("observer condition timeout: %s", reason) } func TestObserverCollectsEntries(t *testing.T) { defer StopStacks() var buf bytes.Buffer logger := newStructuredTestLogger(&buf) observer := NewObserver() logger.AppendEntryHandler(observer) logger.WithField("user_id", 42).Info("login ok") waitObserverCount(t, observer, 1, 300*time.Millisecond) entries := observer.Entries() if len(entries) == 0 { t.Fatalf("observer should collect entries") } last := entries[len(entries)-1] if last.Message != "login ok" { t.Fatalf("unexpected observed message: %q", last.Message) } if got, ok := last.Fields["user_id"]; !ok || got != 42 { t.Fatalf("unexpected observed fields: %+v", last.Fields) } } func TestObserverLimitAndDropped(t *testing.T) { defer StopStacks() observer := NewObserverWithLimit(2) logger := newStructuredTestLogger(&bytes.Buffer{}) logger.AppendEntryHandler(observer) logger.Info("one") logger.Info("two") logger.Info("three") waitObserverCondition(t, 400*time.Millisecond, func() bool { if observer.Dropped() == 0 { return false } entries := observer.Entries() if len(entries) != 2 { return false } return entries[0].Message == "two" && entries[1].Message == "three" }, "observer limit should keep newest two entries") entries := observer.Entries() if len(entries) != 2 { t.Fatalf("observer should keep only limited entries, got %d", len(entries)) } if entries[0].Message != "two" || entries[1].Message != "three" { t.Fatalf("observer should keep newest entries, got %q %q", entries[0].Message, entries[1].Message) } if observer.Dropped() == 0 { t.Fatalf("observer dropped count should increase when over limit") } } func TestObserverTakeAllAndReset(t *testing.T) { defer StopStacks() observer := NewObserver() logger := newStructuredTestLogger(&bytes.Buffer{}) logger.AppendEntryHandler(observer) logger.Info("a") logger.Info("b") waitObserverCount(t, observer, 2, 300*time.Millisecond) all := observer.TakeAll() if len(all) != 2 { t.Fatalf("take all should return all collected entries, got %d", len(all)) } if observer.Count() != 0 { t.Fatalf("take all should clear observer entries") } logger.Info("c") waitObserverCount(t, observer, 1, 300*time.Millisecond) observer.Reset() if observer.Count() != 0 || observer.Dropped() != 0 { t.Fatalf("reset should clear observer state") } } func TestTestHookAttachAndRestore(t *testing.T) { defer StopStacks() var buf bytes.Buffer logger := newStructuredTestLogger(&buf) var previousCount uint64 logger.SetEntryHandler(HandlerFunc(func(_ context.Context, _ *Entry) error { atomic.AddUint64(&previousCount, 1) return nil })) hook := NewTestHook(logger) if hook == nil { t.Fatalf("new test hook should not be nil") } logger.Info("hooked") waitObserverCount(t, hook.Observer(), 1, 300*time.Millisecond) if atomic.LoadUint64(&previousCount) == 0 { t.Fatalf("test hook should keep previous handler in chain") } last, ok := hook.Last() if !ok || last.Message != "hooked" { t.Fatalf("unexpected hook last entry: ok=%v msg=%q", ok, last.Message) } if !hook.Close() { t.Fatalf("hook close should restore previous handler when unchanged") } before := hook.Count() logger.Info("after-close") time.Sleep(30 * time.Millisecond) if hook.Count() != before { t.Fatalf("closed hook should stop collecting new entries") } if atomic.LoadUint64(&previousCount) < 2 { t.Fatalf("previous handler should continue working after hook close") } if !strings.Contains(buf.String(), "after-close") { t.Fatalf("logger output should still contain logs after hook close") } } func TestTestHookCloseWhenHandlerReplaced(t *testing.T) { defer StopStacks() logger := newStructuredTestLogger(&bytes.Buffer{}) hook := NewTestHook(logger) logger.SetEntryHandler(nil) if hook.Close() { t.Fatalf("hook close should fail when handler replaced externally") } }