216 lines
5.9 KiB
Go
216 lines
5.9 KiB
Go
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()
|
|
}
|