package starlog import ( "bytes" "context" "errors" "path/filepath" "sync/atomic" "testing" "time" ) type syncWriteBuffer struct { bytes.Buffer syncCalls uint64 } func (buffer *syncWriteBuffer) Sync() error { atomic.AddUint64(&buffer.syncCalls, 1) return nil } type closeSink struct { closeCalls uint64 } func (sink *closeSink) Write(data []byte) error { _ = data return nil } func (sink *closeSink) Close() error { atomic.AddUint64(&sink.closeCalls, 1) return nil } func TestFlushDrainsPendingWrites(t *testing.T) { var output bytes.Buffer logger := newStructuredTestLogger(&output) logger.SetSwitching(true) logger.Infoln("pending") if output.Len() != 0 { t.Fatalf("pending logs should not be written while switching=true") } if err := logger.Flush(); err != nil { t.Fatalf("Flush failed: %v", err) } if output.Len() == 0 { t.Fatalf("Flush should write buffered logs") } } func TestSyncCallsWriterSync(t *testing.T) { var writer syncWriteBuffer logger := NewStarlog(&writer) logger.SetShowStd(false) logger.SetShowColor(false) logger.SetShowOriginFile(false) logger.SetShowFuncName(false) logger.SetShowFlag(false) logger.Infoln("sync") if err := logger.Sync(); err != nil { t.Fatalf("Sync failed: %v", err) } if atomic.LoadUint64(&writer.syncCalls) == 0 { t.Fatalf("Sync should call underlying writer Sync method") } } func TestCloseClosesSinkAndStopsWrite(t *testing.T) { sink := &closeSink{} logger := NewStarlog(nil) logger.SetShowStd(false) logger.SetShowColor(false) logger.SetShowOriginFile(false) logger.SetShowFuncName(false) logger.SetShowFlag(false) logger.SetSink(sink) if err := logger.Close(); err != nil { t.Fatalf("Close failed: %v", err) } if atomic.LoadUint64(&sink.closeCalls) == 0 { t.Fatalf("Close should call sink.Close") } if !logger.IsWriteStopped() { t.Fatalf("Close should stop writer") } } func TestWaitAsyncDrainContextTimeout(t *testing.T) { stackMu.Lock() stackStarted = true stacks = newStarChanStack(1) stackStopChan = nil stackDoneChan = nil stackMu.Unlock() defer func() { stackMu.Lock() if stacks != nil { _ = stacks.Close() } stackStarted = false stacks = nil stackStopChan = nil stackDoneChan = nil stackMu.Unlock() }() if err := stacks.Push("x"); err != nil { t.Fatalf("prepare queue failed: %v", err) } ctx, cancel := context.WithTimeout(context.Background(), 20*time.Millisecond) defer cancel() err := WaitAsyncDrain(ctx) if !errors.Is(err, context.DeadlineExceeded) { t.Fatalf("WaitAsyncDrain should return context deadline, got %v", err) } } func TestShutdownStopsAsyncStacks(t *testing.T) { resetAsyncMetricsForTest() defer func() { resetAsyncMetricsForTest() StopStacks() }() logger := NewStarlog(nil) logger.SetShowStd(false) handled := make(chan struct{}, 1) logger.SetHandler(func(LogData) { select { case handled <- struct{}{}: default: } }) logger.Infoln("shutdown") ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() if err := logger.Shutdown(ctx); err != nil { t.Fatalf("Shutdown failed: %v", err) } select { case <-handled: case <-time.After(200 * time.Millisecond): t.Fatalf("async handler should complete before shutdown") } stackMu.Lock() started := stackStarted stackMu.Unlock() if started { t.Fatalf("Shutdown should stop async stacks") } } func TestCloseManagedLogFileNoDoubleCloseError(t *testing.T) { logger := NewStarlog(nil) logger.SetShowStd(false) logPath := filepath.Join(testBinDir(t), "lifecycle_close.log") if err := SetLogFile(logPath, logger, false); err != nil { t.Fatalf("SetLogFile failed: %v", err) } if err := logger.Close(); err != nil { t.Fatalf("Close should not fail for managed log file: %v", err) } }