FILE / ScuroNeko/SNekLog

logger_test.go

Исходный файл и его история в репозитории.
FILE v2.0.1
Files
SNekLog/logger_test.go
T
ScuroNeko 19f8750d35
Golang lint / lint (push) Failing after 48s
(fix): module declaration slog->sneklog
2026-04-27 09:56:46 +03:00

337 lines
9.4 KiB
Go

package sneklog
import (
"bytes"
"encoding/json"
"errors"
"os"
"strings"
"testing"
)
type stubLoggerWriter struct {
printErr error
closeErr error
printCalls int
closeCalls int
messages []any
}
func (w *stubLoggerWriter) Formatter() *Formatter {
return &Formatter{}
}
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{})
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)
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 TestFormatterHandlesEmptyTracebackPlaceholders(t *testing.T) {
formatter := NewFormatter().
SetFormat("%m|%b|%B|%M|%f|%n|%s|%p")
got := formatter.FormatMessage(INFO, "TEST", nil, "hello")
if got != "hello|||||||" {
t.Fatalf("empty traceback placeholders should be empty, got %q", got)
}
}
func TestFormatterDoesNotInterpretPlaceholdersInsideMessages(t *testing.T) {
formatter := NewFormatter().
SetFormat("%L:%m")
message := "literal %L %s %p"
got := formatter.FormatMessage(INFO, "TEST", nil, message)
if got != "INFO:literal %L %s %p" {
t.Fatalf("message placeholder-looking text should stay literal, got %q", got)
}
}
func TestNewFormatterDoesNotMutateDefaultFormatter(t *testing.T) {
formatter := NewFormatter()
formatter.SetFormat("custom")
if DefaultTextFormatter.Format == "custom" {
t.Fatal("NewFormatter() should return a copy, not mutate DefaultTextFormatter")
}
}
func TestTextWriterColorOutputHonorsColorOnlyStdoutFalse(t *testing.T) {
var buf bytes.Buffer
formatter := NewFormatter().
SetFormat("%m").
SetColorOutput(true).
SetColorOnlyStdout(false)
writer := CreateTextWriter(&buf).SetFormatter(formatter)
if err := writer.Print(INFO, "TEST", nil, "hello"); err != nil {
t.Fatalf("Print() error = %v", err)
}
if !strings.Contains(buf.String(), "\x1b[") {
t.Fatalf("ColorOnlyStdout(false) should allow color for external writers, got %q", buf.String())
}
}
func TestTextWriterColorOutputCanBeDisabled(t *testing.T) {
var buf bytes.Buffer
formatter := NewFormatter().
SetFormat("%m").
SetColorOutput(false).
SetColorOnlyStdout(false)
writer := CreateTextWriter(&buf).SetFormatter(formatter)
if err := writer.Print(INFO, "TEST", nil, "hello"); err != nil {
t.Fatalf("Print() error = %v", err)
}
if strings.Contains(buf.String(), "\x1b[") {
t.Fatalf("SetColorOutput(false) should disable color output, got %q", buf.String())
}
}
func TestCreateTextStdoutWriterDoesNotCloseStdout(t *testing.T) {
stdoutFile := swapStdout(t)
writer := CreateTextStdoutWriter()
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 TestGetFullTracebackSkipsRuntimeAndSneklogFrames(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 sneklog 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 sneklog 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()
}