package starlog import ( "bytes" "context" "errors" "io" "strings" "sync/atomic" "testing" "time" ) func TestWriteBufferFlushAfterSwitchingOff(t *testing.T) { var buf bytes.Buffer logger := NewStarlog(&buf) logger.SetShowStd(false) logger.SetShowColor(false) logger.SetSwitching(true) logger.Infoln("first") logger.Infoln("second") if got := buf.String(); strings.Contains(got, "first") || strings.Contains(got, "second") { t.Fatalf("logs should stay buffered while switching=true, got: %q", got) } logger.SetSwitching(false) got := buf.String() if !strings.Contains(got, "first") || !strings.Contains(got, "second") { t.Fatalf("buffered logs were not flushed after switching=false, got: %q", got) } } func TestAsyncQueuePushFailureFallsBackToSyncHandler(t *testing.T) { resetAsyncMetricsForTest() defer func() { resetAsyncMetricsForTest() stackMu.Lock() if stacks != nil { _ = stacks.Close() } stackStarted = false stacks = nil stackStopChan = nil stackDoneChan = nil stackMu.Unlock() }() logger := NewStarlog(nil) logger.SetShowStd(false) var handled uint64 logger.handlerFunc = func(data LogData) { atomic.AddUint64(&handled, 1) } alertCalled := make(chan struct{}, 1) SetAsyncErrorHandler(func(err error, data LogData) { select { case alertCalled <- struct{}{}: default: } }) stackMu.Lock() stackStarted = true stacks = nil stackStopChan = nil stackDoneChan = nil stackMu.Unlock() logger.Infoln("trigger async fallback") if atomic.LoadUint64(&handled) != 1 { t.Fatalf("handler should be called once via sync fallback, got: %d", handled) } if GetAsyncDropCount() == 0 { t.Fatalf("async drop counter should increase on push failure") } select { case <-alertCalled: default: t.Fatalf("async alert handler should be invoked on push failure") } } func TestStarChanStackPushClosedReturnsEOF(t *testing.T) { stack := newStarChanStack(1) if err := stack.Close(); err != nil { t.Fatalf("Close failed: %v", err) } if err := stack.Push("x"); err != io.EOF { t.Fatalf("Push on closed stack should return io.EOF, got: %v", err) } } func TestStarChanStackTryPushFullReturnsQueueFull(t *testing.T) { stack := newStarChanStack(1) if err := stack.Push("first"); err != nil { t.Fatalf("Push failed: %v", err) } if err := stack.TryPush("second"); !errors.Is(err, errStackFull) { t.Fatalf("TryPush on full queue should return errStackFull, got: %v", err) } } func TestPendingWriteLimitDropOldest(t *testing.T) { var buf bytes.Buffer logger := NewStarlog(&buf) logger.SetShowStd(false) logger.SetShowColor(false) logger.SetPendingWriteLimit(2) logger.SetPendingDropPolicy(PendingDropOldest) logger.SetSwitching(true) logger.Infoln("one") logger.Infoln("two") logger.Infoln("three") logger.SetSwitching(false) got := buf.String() if strings.Contains(got, "one") { t.Fatalf("oldest pending log should be dropped, got %q", got) } if !strings.Contains(got, "two") || !strings.Contains(got, "three") { t.Fatalf("newer pending logs should remain, got %q", got) } if logger.GetPendingDropCount() != 1 { t.Fatalf("expected one dropped pending write, got %d", logger.GetPendingDropCount()) } } func TestPendingWriteLimitBlockPolicy(t *testing.T) { var buf bytes.Buffer logger := NewStarlog(&buf) logger.SetShowStd(false) logger.SetShowColor(false) logger.SetPendingWriteLimit(1) logger.SetPendingDropPolicy(PendingBlock) logger.SetSwitching(true) logger.Infoln("one") done := make(chan struct{}) go func() { logger.Infoln("two") close(done) }() blocked := false deadline := time.Now().Add(200 * time.Millisecond) for time.Now().Before(deadline) { select { case <-done: t.Fatalf("block policy should wait while queue is full and switching=true") default: } if logger.GetPendingBlockCount() > 0 { blocked = true break } time.Sleep(5 * time.Millisecond) } if !blocked { t.Fatalf("expected pending block count to increase") } logger.SetSwitching(false) select { case <-done: case <-time.After(300 * time.Millisecond): t.Fatalf("blocked write should continue after switching=false") } got := buf.String() if !strings.Contains(got, "one") || !strings.Contains(got, "two") { t.Fatalf("both logs should be persisted, got %q", got) } if logger.GetPendingDropCount() != 0 { t.Fatalf("block policy should avoid drops, got %d", logger.GetPendingDropCount()) } stats := logger.GetPendingStats() if stats.BlockCount == 0 { t.Fatalf("pending stats should expose block count") } } func TestPendingStatsSnapshot(t *testing.T) { logger := NewStarlog(nil) logger.SetShowStd(false) logger.SetPendingWriteLimit(3) logger.SetPendingDropPolicy(PendingDropNewest) logger.SetSwitching(true) logger.Infoln("one") logger.Infoln("two") stats := logger.GetPendingStats() if stats.Limit != 3 { t.Fatalf("unexpected limit: %d", stats.Limit) } if stats.Length != 2 { t.Fatalf("unexpected pending length: %d", stats.Length) } if stats.Policy != PendingDropNewest { t.Fatalf("unexpected policy: %v", stats.Policy) } if !stats.Switching { t.Fatalf("expected switching flag true in stats snapshot") } if stats.PeakLength < 2 { t.Fatalf("expected peak length >= 2, got %d", stats.PeakLength) } logger.SetSwitching(false) } type errWriter struct{} func (w *errWriter) Write(p []byte) (int, error) { return 0, errors.New("write failed") } func TestWriteErrorObservable(t *testing.T) { resetAsyncMetricsForTest() defer resetAsyncMetricsForTest() logger := NewStarlog(&errWriter{}) logger.SetShowStd(false) logger.SetShowColor(false) observed := make(chan struct{}, 1) SetWriteErrorHandler(func(err error, data LogData) { if err != nil { select { case observed <- struct{}{}: default: } } }) logger.Infoln("write error check") if GetWriteErrorCount() == 0 { t.Fatalf("write error count should increase") } select { case <-observed: default: t.Fatalf("write error handler should be invoked") } } func TestAsyncHandlerPanicDoesNotCrash(t *testing.T) { resetAsyncMetricsForTest() defer func() { resetAsyncMetricsForTest() StopStacks() }() logger := NewStarlog(nil) logger.SetShowStd(false) logger.SetHandler(func(LogData) { panic("boom") }) logger.Infoln("panic safe") time.Sleep(20 * time.Millisecond) if GetAsyncDropCount() == 0 { t.Fatalf("panic in async handler should be reported as drop") } } func TestEntryHandlerTimeoutFallback(t *testing.T) { resetAsyncMetricsForTest() defer func() { resetAsyncMetricsForTest() StopStacks() }() logger := NewStarlog(nil) logger.SetShowStd(false) logger.SetEntryHandler(HandlerFunc(func(context.Context, *Entry) error { time.Sleep(80 * time.Millisecond) return nil })) logger.SetEntryHandlerTimeout(10 * time.Millisecond) begin := time.Now() logger.Infoln("entry timeout") cost := time.Since(begin) if cost > 60*time.Millisecond { t.Fatalf("entry handler timeout should protect main path, took %v", cost) } deadline := time.Now().Add(300 * time.Millisecond) for time.Now().Before(deadline) { if GetAsyncDropCount() > 0 { return } time.Sleep(5 * time.Millisecond) } t.Fatalf("entry handler timeout should be observable via async drop count") } func TestEntryHandlerQueueFullNoFallbackDoesNotBlock(t *testing.T) { resetAsyncMetricsForTest() defer func() { resetAsyncMetricsForTest() stackMu.Lock() stackStarted = false stacks = nil stackStopChan = nil stackDoneChan = nil stackMu.Unlock() }() SetAsyncFallbackToSync(false) logger := NewStarlog(nil) logger.SetShowStd(false) var handled uint64 logger.SetEntryHandler(HandlerFunc(func(context.Context, *Entry) error { atomic.AddUint64(&handled, 1) time.Sleep(80 * time.Millisecond) return nil })) stackMu.Lock() stackStarted = true stacks = newStarChanStack(1) stackStopChan = nil stackDoneChan = nil stackMu.Unlock() if err := stacks.Push(logTransfer{ handlerFunc: func(LogData) { time.Sleep(80 * time.Millisecond) }, }); err != nil { t.Fatalf("prepare full async queue failed: %v", err) } begin := time.Now() logger.Infoln("entry queue full") cost := time.Since(begin) if cost > 60*time.Millisecond { t.Fatalf("entry handler should not block when queue is full, took %v", cost) } if atomic.LoadUint64(&handled) != 0 { t.Fatalf("entry handler should be dropped when queue is full and fallback disabled, got %d", handled) } if GetAsyncDropCount() == 0 { t.Fatalf("queue-full drop should be observable") } } func TestWriteMethodNameCompatibility(t *testing.T) { logger := NewStarlog(nil) logger.StopWrite() if !logger.IsWriteStopped() || !logger.IsWriteStoed() { t.Fatalf("both write-stop getters should report true") } logger.EnableWrite() if logger.IsWriteStopped() || logger.IsWriteStoed() { t.Fatalf("EnableWrite should resume writer for both getter names") } } func TestLevelFilterAPI(t *testing.T) { var buf bytes.Buffer logger := NewStarlog(&buf) logger.SetShowStd(false) logger.SetShowColor(false) logger.SetShowOriginFile(false) logger.SetShowFuncName(false) logger.SetShowFlag(false) if !logger.IsLevelEnabled(LvDebug) { t.Fatalf("debug level should be enabled by default") } logger.SetLevel(LvWarning) if logger.GetLevel() != LvWarning { t.Fatalf("unexpected level threshold: %d", logger.GetLevel()) } if logger.IsLevelEnabled(LvInfo) { t.Fatalf("info should be filtered by warning threshold") } if !logger.IsLevelEnabled(LvError) { t.Fatalf("error should be enabled by warning threshold") } logger.Infoln("filtered") logger.Warningln("visible") logStr := buf.String() if strings.Contains(logStr, "filtered") { t.Fatalf("info log should be filtered, got %q", logStr) } if !strings.Contains(logStr, "visible") { t.Fatalf("warning log should remain, got %q", logStr) } } func TestParseLevel(t *testing.T) { tests := map[string]int{ "debug": LvDebug, "INFO": LvInfo, "notice": LvNotice, "warn": LvWarning, "warning": LvWarning, "err": LvError, "critical": LvCritical, "panic": LvPanic, "fatal": LvFatal, "7": 7, } for input, expected := range tests { parsed, err := ParseLevel(input) if err != nil { t.Fatalf("ParseLevel(%q) returned error: %v", input, err) } if parsed != expected { t.Fatalf("ParseLevel(%q)=%d, want %d", input, parsed, expected) } } _, err := ParseLevel("unknown-level") if !errors.Is(err, ErrInvalidLevel) { t.Fatalf("ParseLevel invalid input should return ErrInvalidLevel, got %v", err) } }