starlog/p0_reliability_test.go

433 lines
10 KiB
Go
Raw Normal View History

2026-03-19 16:37:57 +08:00
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)
}
}