package slog import ( "bytes" "errors" "os" "strings" "testing" ) type stubLoggerWriter struct { printErr error closeErr error printCalls int closeCalls int } func (w *stubLoggerWriter) Close() error { w.closeCalls++ return w.closeErr } func (w *stubLoggerWriter) Write(p []byte) (int, error) { return len(p), nil } func (w *stubLoggerWriter) Print(_ LogLevel, _ string, _ []*MethodTraceback, _ ...any) error { w.printCalls++ return w.printErr } func TestLoggerCloseReturnsJoinedErrorsWithoutRelogging(t *testing.T) { firstErr := errors.New("first close failed") secondErr := errors.New("second close failed") first := &stubLoggerWriter{closeErr: firstErr} second := &stubLoggerWriter{closeErr: secondErr} logger := CreateLogger().AddWriters(first, second) err := logger.Close() if !errors.Is(err, firstErr) { t.Fatalf("Close() error should include first writer error, got %v", err) } if !errors.Is(err, secondErr) { t.Fatalf("Close() error should include second writer error, got %v", err) } if first.printCalls != 0 || second.printCalls != 0 { t.Fatalf("Close() should not log through writers again, got print calls first=%d second=%d", first.printCalls, second.printCalls) } if first.closeCalls != 1 || second.closeCalls != 1 { t.Fatalf("Close() should close each writer once, got close calls first=%d second=%d", first.closeCalls, second.closeCalls) } if err := logger.Close(); err != nil { t.Fatalf("second Close() should be a no-op, got %v", err) } } func TestLoggerPrintDoesNotRecurseOnWriterError(t *testing.T) { oldStderr := os.Stderr stderrFile, err := os.CreateTemp(t.TempDir(), "stderr") if err != nil { t.Fatalf("CreateTemp() error = %v", err) } os.Stderr = stderrFile t.Cleanup(func() { os.Stderr = oldStderr _ = stderrFile.Close() }) bad := &stubLoggerWriter{printErr: errors.New("print failed")} good := &stubLoggerWriter{} logger := CreateLogger().AddWriters(bad, good) logger.Error("boom") if bad.printCalls != 1 { t.Fatalf("bad writer should be called once, got %d", bad.printCalls) } if good.printCalls != 1 { t.Fatalf("good writer should be called once, got %d", good.printCalls) } } func TestCreateTextWriterCloseOnNonCloserIsNoOp(t *testing.T) { writer := CreateTextWriter(&bytes.Buffer{}, false, false) if err := writer.Close(); err != nil { t.Fatalf("Close() error = %v", err) } } func TestCreateTextWriterDoesNotCloseExternalCloser(t *testing.T) { file, err := os.CreateTemp(t.TempDir(), "text-writer") if err != nil { t.Fatalf("CreateTemp() error = %v", err) } t.Cleanup(func() { _ = file.Close() }) writer := CreateTextWriter(file, false, false) if err := writer.Close(); err != nil { t.Fatalf("Close() error = %v", err) } if _, err := file.WriteString("still open"); err != nil { t.Fatalf("CreateTextWriter() should not close external writers, got %v", err) } } func TestCreateJsonWriterCloseOnNonCloserIsNoOp(t *testing.T) { writer := CreateJsonWriter(&bytes.Buffer{}, false) if err := writer.Close(); err != nil { t.Fatalf("Close() error = %v", err) } } func TestCreateJsonWriterDoesNotCloseExternalCloser(t *testing.T) { file, err := os.CreateTemp(t.TempDir(), "json-writer") if err != nil { t.Fatalf("CreateTemp() error = %v", err) } t.Cleanup(func() { _ = file.Close() }) writer := CreateJsonWriter(file, false) if err := writer.Close(); err != nil { t.Fatalf("Close() error = %v", err) } if _, err := file.WriteString("still open"); err != nil { t.Fatalf("CreateJsonWriter() should not close external writers, got %v", err) } } func TestCreateTextStdoutWriterDoesNotCloseStdout(t *testing.T) { stdoutFile := swapStdout(t) writer := CreateTextStdoutWriter(false, false) if err := writer.Close(); err != nil { t.Fatalf("Close() error = %v", err) } if _, err := stdoutFile.WriteString("still open"); err != nil { t.Fatalf("stdout writer Close() should not close stdout, got %v", err) } } func TestCreateJsonStdoutWriterDoesNotCloseStdout(t *testing.T) { stdoutFile := swapStdout(t) writer := CreateJsonStdoutWriter(false) if err := writer.Close(); err != nil { t.Fatalf("Close() error = %v", err) } if _, err := stdoutFile.WriteString("still open"); err != nil { t.Fatalf("stdout writer Close() should not close stdout, got %v", err) } } func TestGetFullTracebackSkipsRuntimeAndSlogFrames(t *testing.T) { tracebacks := captureTracebackForTest() if len(tracebacks) == 0 { t.Fatal("expected at least one traceback frame") } first := tracebacks[0] if first.Method != "captureTracebackForTest" { t.Fatalf("first frame should be the nearest user frame, got %s", first.Method) } for _, tb := range tracebacks { if strings.HasPrefix(tb.Signature, "runtime.") { t.Fatalf("runtime frame should be filtered out, got %s", tb.Signature) } if _, ok := internalTracebackMethods[tb.Method]; ok { t.Fatalf("internal slog frame should be filtered out, got %s", tb.Signature) } } } func TestGetTracebackReturnsNearestUserFrame(t *testing.T) { tb := captureSingleTracebackForTest() if tb == nil { t.Fatal("expected traceback frame") } if tb.Method != "captureSingleTracebackForTest" { t.Fatalf("expected nearest user frame, got %s", tb.Method) } if strings.HasPrefix(tb.Signature, "runtime.") { t.Fatalf("runtime frame should be filtered out, got %s", tb.Signature) } if _, ok := internalTracebackMethods[tb.Method]; ok { t.Fatalf("internal slog frame should be filtered out, got %s", tb.Signature) } } func swapStdout(t *testing.T) *os.File { t.Helper() oldStdout := os.Stdout stdoutFile, err := os.CreateTemp(t.TempDir(), "stdout") if err != nil { t.Fatalf("CreateTemp() error = %v", err) } os.Stdout = stdoutFile t.Cleanup(func() { os.Stdout = oldStdout _ = stdoutFile.Close() }) return stdoutFile } func captureTracebackForTest() []*MethodTraceback { return getFullTraceback(0) } func captureSingleTracebackForTest() *MethodTraceback { return getTraceback() }