FILE / ScuroNeko/SNekLog
logger_test.go
Исходный файл и его история в репозитории.
Add Logger.AddReplacer to mask or normalize message text before records reach writers. Document replacement behavior in README and package GoDoc, and update the example. Also fix JSON writer edge cases: - avoid panic on empty message lists - preserve newline semantics when the last message already ends with n - keep original message argument types for custom writers when no replacers are configured Update dependencies and add release notes for the next release.
272 lines
7.5 KiB
Go
272 lines
7.5 KiB
Go
package slog
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
type stubLoggerWriter struct {
|
|
printErr error
|
|
closeErr error
|
|
printCalls int
|
|
closeCalls int
|
|
messages []any
|
|
}
|
|
|
|
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, messages ...any) error {
|
|
w.printCalls++
|
|
w.messages = append([]any(nil), messages...)
|
|
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 TestLoggerPreservesMessageTypesWithoutReplacers(t *testing.T) {
|
|
writer := &stubLoggerWriter{}
|
|
logger := CreateLogger().AddWriter(writer)
|
|
|
|
logger.Error("status", 500)
|
|
|
|
if len(writer.messages) != 2 {
|
|
t.Fatalf("writer should receive two messages, got %d", len(writer.messages))
|
|
}
|
|
if _, ok := writer.messages[1].(int); !ok {
|
|
t.Fatalf("writer should receive original int message type, got %T", writer.messages[1])
|
|
}
|
|
}
|
|
|
|
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 TestJsonWriterPrintAllowsEmptyMessages(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
writer := CreateJsonWriter(&buf, false)
|
|
|
|
if err := writer.Print(INFO, "TEST", nil); err != nil {
|
|
t.Fatalf("Print() error = %v", err)
|
|
}
|
|
|
|
var message LoggerJsonMessage
|
|
if err := json.Unmarshal(buf.Bytes(), &message); err != nil {
|
|
t.Fatalf("json.Unmarshal() error = %v", err)
|
|
}
|
|
if message.Message != "" {
|
|
t.Fatalf("message should be empty, got %q", message.Message)
|
|
}
|
|
}
|
|
|
|
func TestJsonWriterPrintPreservesTrailingNewlineSemantic(t *testing.T) {
|
|
var buf bytes.Buffer
|
|
writer := CreateJsonWriter(&buf, false)
|
|
|
|
if err := writer.Print(INFO, "TEST", nil, "hello\n"); err != nil {
|
|
t.Fatalf("Print() error = %v", err)
|
|
}
|
|
|
|
data := buf.Bytes()
|
|
if !bytes.HasSuffix(data, []byte("\n")) {
|
|
t.Fatalf("JSON output should end with newline, got %q", data)
|
|
}
|
|
|
|
var message LoggerJsonMessage
|
|
if err := json.Unmarshal(bytes.TrimSuffix(data, []byte("\n")), &message); err != nil {
|
|
t.Fatalf("json.Unmarshal() error = %v", err)
|
|
}
|
|
if message.Message != "hello" {
|
|
t.Fatalf("message should not include trailing newline, got %q", message.Message)
|
|
}
|
|
}
|
|
|
|
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()
|
|
}
|