2026-03-06 09:30:57 +00:00
|
|
|
package log
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bytes"
|
|
|
|
|
"errors"
|
|
|
|
|
"fmt"
|
|
|
|
|
"strings"
|
|
|
|
|
"testing"
|
2026-03-30 06:33:30 +00:00
|
|
|
"time"
|
2026-03-06 09:30:57 +00:00
|
|
|
|
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// --- Err Type Tests ---
|
|
|
|
|
|
|
|
|
|
func TestErr_Error_Good(t *testing.T) {
|
|
|
|
|
// With underlying error
|
|
|
|
|
err := &Err{Op: "db.Query", Msg: "failed to query", Err: errors.New("connection refused")}
|
|
|
|
|
assert.Equal(t, "db.Query: failed to query: connection refused", err.Error())
|
|
|
|
|
|
|
|
|
|
// With code
|
|
|
|
|
err = &Err{Op: "api.Call", Msg: "request failed", Code: "TIMEOUT"}
|
|
|
|
|
assert.Equal(t, "api.Call: request failed [TIMEOUT]", err.Error())
|
|
|
|
|
|
|
|
|
|
// With both underlying error and code
|
|
|
|
|
err = &Err{Op: "user.Save", Msg: "save failed", Err: errors.New("duplicate key"), Code: "DUPLICATE"}
|
|
|
|
|
assert.Equal(t, "user.Save: save failed [DUPLICATE]: duplicate key", err.Error())
|
|
|
|
|
|
|
|
|
|
// Just op and msg
|
|
|
|
|
err = &Err{Op: "cache.Get", Msg: "miss"}
|
|
|
|
|
assert.Equal(t, "cache.Get: miss", err.Error())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestErr_Error_EmptyOp_Good(t *testing.T) {
|
|
|
|
|
// No Op - should not have leading colon
|
|
|
|
|
err := &Err{Msg: "just a message"}
|
|
|
|
|
assert.Equal(t, "just a message", err.Error())
|
|
|
|
|
|
|
|
|
|
// No Op with code
|
|
|
|
|
err = &Err{Msg: "error with code", Code: "ERR_CODE"}
|
|
|
|
|
assert.Equal(t, "error with code [ERR_CODE]", err.Error())
|
|
|
|
|
|
|
|
|
|
// No Op with underlying error
|
|
|
|
|
err = &Err{Msg: "wrapped", Err: errors.New("underlying")}
|
|
|
|
|
assert.Equal(t, "wrapped: underlying", err.Error())
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-30 13:36:30 +00:00
|
|
|
func TestErr_Error_EmptyMsg_Good(t *testing.T) {
|
|
|
|
|
err := &Err{Op: "api.Call", Code: "TIMEOUT"}
|
|
|
|
|
assert.Equal(t, "api.Call: [TIMEOUT]", err.Error())
|
|
|
|
|
|
|
|
|
|
err = &Err{Op: "api.Call", Err: errors.New("underlying")}
|
|
|
|
|
assert.Equal(t, "api.Call: underlying", err.Error())
|
|
|
|
|
|
|
|
|
|
err = &Err{Op: "api.Call", Code: "TIMEOUT", Err: errors.New("underlying")}
|
|
|
|
|
assert.Equal(t, "api.Call: [TIMEOUT]: underlying", err.Error())
|
|
|
|
|
|
|
|
|
|
err = &Err{Op: "api.Call"}
|
|
|
|
|
assert.Equal(t, "api.Call", err.Error())
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-06 09:30:57 +00:00
|
|
|
func TestErr_Unwrap_Good(t *testing.T) {
|
|
|
|
|
underlying := errors.New("underlying error")
|
|
|
|
|
err := &Err{Op: "test", Msg: "wrapped", Err: underlying}
|
|
|
|
|
|
|
|
|
|
assert.Equal(t, underlying, errors.Unwrap(err))
|
|
|
|
|
assert.True(t, errors.Is(err, underlying))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- Error Creation Function Tests ---
|
|
|
|
|
|
|
|
|
|
func TestE_Good(t *testing.T) {
|
|
|
|
|
underlying := errors.New("base error")
|
|
|
|
|
err := E("op.Name", "something failed", underlying)
|
|
|
|
|
|
|
|
|
|
assert.NotNil(t, err)
|
|
|
|
|
var logErr *Err
|
|
|
|
|
assert.True(t, errors.As(err, &logErr))
|
|
|
|
|
assert.Equal(t, "op.Name", logErr.Op)
|
|
|
|
|
assert.Equal(t, "something failed", logErr.Msg)
|
|
|
|
|
assert.Equal(t, underlying, logErr.Err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestE_Good_NilError(t *testing.T) {
|
|
|
|
|
// E creates an error even with nil underlying - useful for errors without causes
|
|
|
|
|
err := E("op.Name", "message", nil)
|
|
|
|
|
assert.NotNil(t, err)
|
|
|
|
|
assert.Equal(t, "op.Name: message", err.Error())
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-30 06:33:30 +00:00
|
|
|
func TestEWithRecovery_Good(t *testing.T) {
|
|
|
|
|
retryAfter := time.Second * 5
|
|
|
|
|
err := EWithRecovery("op.Name", "message", nil, true, &retryAfter, "retry once")
|
|
|
|
|
|
|
|
|
|
var logErr *Err
|
|
|
|
|
assert.NotNil(t, err)
|
|
|
|
|
assert.True(t, As(err, &logErr))
|
|
|
|
|
assert.True(t, logErr.Retryable)
|
|
|
|
|
if assert.NotNil(t, logErr.RetryAfter) {
|
|
|
|
|
assert.Equal(t, retryAfter, *logErr.RetryAfter)
|
|
|
|
|
}
|
|
|
|
|
assert.Equal(t, "retry once", logErr.NextAction)
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-06 09:30:57 +00:00
|
|
|
func TestWrap_Good(t *testing.T) {
|
|
|
|
|
underlying := errors.New("base")
|
|
|
|
|
err := Wrap(underlying, "handler.Process", "processing failed")
|
|
|
|
|
|
|
|
|
|
assert.NotNil(t, err)
|
|
|
|
|
assert.Contains(t, err.Error(), "handler.Process")
|
|
|
|
|
assert.Contains(t, err.Error(), "processing failed")
|
|
|
|
|
assert.True(t, errors.Is(err, underlying))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestWrap_PreservesCode_Good(t *testing.T) {
|
|
|
|
|
// Create an error with a code
|
|
|
|
|
inner := WrapCode(errors.New("base"), "VALIDATION_ERROR", "inner.Op", "validation failed")
|
|
|
|
|
|
|
|
|
|
// Wrap it - should preserve the code
|
|
|
|
|
outer := Wrap(inner, "outer.Op", "outer context")
|
|
|
|
|
|
|
|
|
|
assert.NotNil(t, outer)
|
|
|
|
|
assert.Equal(t, "VALIDATION_ERROR", ErrCode(outer))
|
|
|
|
|
assert.Contains(t, outer.Error(), "[VALIDATION_ERROR]")
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-30 06:33:30 +00:00
|
|
|
func TestWrap_PreservesRecovery_Good(t *testing.T) {
|
|
|
|
|
retryAfter := 15 * time.Second
|
|
|
|
|
inner := &Err{Msg: "inner", Retryable: true, RetryAfter: &retryAfter, NextAction: "inspect input"}
|
|
|
|
|
|
|
|
|
|
outer := Wrap(inner, "outer.Op", "outer context")
|
|
|
|
|
|
|
|
|
|
assert.NotNil(t, outer)
|
|
|
|
|
var logErr *Err
|
|
|
|
|
assert.True(t, As(outer, &logErr))
|
|
|
|
|
assert.True(t, logErr.Retryable)
|
|
|
|
|
if assert.NotNil(t, logErr.RetryAfter) {
|
|
|
|
|
assert.Equal(t, retryAfter, *logErr.RetryAfter)
|
|
|
|
|
}
|
|
|
|
|
assert.Equal(t, "inspect input", logErr.NextAction)
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-29 22:39:46 +00:00
|
|
|
func TestWrap_PreservesCode_FromNestedChain_Good(t *testing.T) {
|
|
|
|
|
root := WrapCode(errors.New("base"), "CHAIN_ERROR", "inner", "inner failed")
|
|
|
|
|
wrapped := Wrap(fmt.Errorf("mid layer: %w", root), "outer", "outer context")
|
|
|
|
|
|
|
|
|
|
assert.Equal(t, "CHAIN_ERROR", ErrCode(wrapped))
|
|
|
|
|
assert.Contains(t, wrapped.Error(), "[CHAIN_ERROR]")
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-06 09:30:57 +00:00
|
|
|
func TestWrap_NilError_Good(t *testing.T) {
|
|
|
|
|
err := Wrap(nil, "op", "msg")
|
|
|
|
|
assert.Nil(t, err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestWrapCode_Good(t *testing.T) {
|
|
|
|
|
underlying := errors.New("validation failed")
|
|
|
|
|
err := WrapCode(underlying, "INVALID_INPUT", "api.Validate", "bad request")
|
|
|
|
|
|
|
|
|
|
assert.NotNil(t, err)
|
|
|
|
|
var logErr *Err
|
|
|
|
|
assert.True(t, errors.As(err, &logErr))
|
|
|
|
|
assert.Equal(t, "INVALID_INPUT", logErr.Code)
|
|
|
|
|
assert.Equal(t, "api.Validate", logErr.Op)
|
|
|
|
|
assert.Contains(t, err.Error(), "[INVALID_INPUT]")
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-30 06:33:30 +00:00
|
|
|
func TestWrapCodeWithRecovery_Good(t *testing.T) {
|
|
|
|
|
retryAfter := time.Minute
|
|
|
|
|
err := WrapCodeWithRecovery(errors.New("validation failed"), "INVALID_INPUT", "api.Validate", "bad request", true, &retryAfter, "retry with backoff")
|
|
|
|
|
|
|
|
|
|
var logErr *Err
|
|
|
|
|
assert.NotNil(t, err)
|
|
|
|
|
assert.True(t, As(err, &logErr))
|
|
|
|
|
assert.True(t, logErr.Retryable)
|
|
|
|
|
assert.NotNil(t, logErr.RetryAfter)
|
|
|
|
|
assert.Equal(t, retryAfter, *logErr.RetryAfter)
|
|
|
|
|
assert.Equal(t, "retry with backoff", logErr.NextAction)
|
|
|
|
|
assert.Equal(t, "INVALID_INPUT", logErr.Code)
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-06 09:30:57 +00:00
|
|
|
func TestWrapCode_Good_NilError(t *testing.T) {
|
|
|
|
|
// WrapCode with nil error but with code still creates an error
|
|
|
|
|
err := WrapCode(nil, "CODE", "op", "msg")
|
|
|
|
|
assert.NotNil(t, err)
|
|
|
|
|
assert.Contains(t, err.Error(), "[CODE]")
|
|
|
|
|
|
|
|
|
|
// Only returns nil when both error and code are empty
|
|
|
|
|
err = WrapCode(nil, "", "op", "msg")
|
|
|
|
|
assert.Nil(t, err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestNewCode_Good(t *testing.T) {
|
|
|
|
|
err := NewCode("NOT_FOUND", "resource not found")
|
|
|
|
|
|
|
|
|
|
var logErr *Err
|
|
|
|
|
assert.True(t, errors.As(err, &logErr))
|
|
|
|
|
assert.Equal(t, "NOT_FOUND", logErr.Code)
|
|
|
|
|
assert.Equal(t, "resource not found", logErr.Msg)
|
|
|
|
|
assert.Nil(t, logErr.Err)
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-30 06:33:30 +00:00
|
|
|
func TestNewCodeWithRecovery_Good(t *testing.T) {
|
|
|
|
|
retryAfter := 2 * time.Minute
|
|
|
|
|
err := NewCodeWithRecovery("NOT_FOUND", "resource not found", false, &retryAfter, "contact support")
|
|
|
|
|
|
|
|
|
|
var logErr *Err
|
|
|
|
|
assert.NotNil(t, err)
|
|
|
|
|
assert.True(t, As(err, &logErr))
|
|
|
|
|
assert.False(t, logErr.Retryable)
|
|
|
|
|
assert.NotNil(t, logErr.RetryAfter)
|
|
|
|
|
assert.Equal(t, retryAfter, *logErr.RetryAfter)
|
|
|
|
|
assert.Equal(t, "contact support", logErr.NextAction)
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-06 09:30:57 +00:00
|
|
|
// --- Standard Library Wrapper Tests ---
|
|
|
|
|
|
|
|
|
|
func TestIs_Good(t *testing.T) {
|
|
|
|
|
sentinel := errors.New("sentinel")
|
|
|
|
|
wrapped := Wrap(sentinel, "test", "wrapped")
|
|
|
|
|
|
|
|
|
|
assert.True(t, Is(wrapped, sentinel))
|
|
|
|
|
assert.False(t, Is(wrapped, errors.New("other")))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestAs_Good(t *testing.T) {
|
|
|
|
|
err := E("test.Op", "message", errors.New("base"))
|
|
|
|
|
|
|
|
|
|
var logErr *Err
|
|
|
|
|
assert.True(t, As(err, &logErr))
|
|
|
|
|
assert.Equal(t, "test.Op", logErr.Op)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestNewError_Good(t *testing.T) {
|
|
|
|
|
err := NewError("simple error")
|
|
|
|
|
assert.NotNil(t, err)
|
|
|
|
|
assert.Equal(t, "simple error", err.Error())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestJoin_Good(t *testing.T) {
|
|
|
|
|
err1 := errors.New("error 1")
|
|
|
|
|
err2 := errors.New("error 2")
|
|
|
|
|
joined := Join(err1, err2)
|
|
|
|
|
|
|
|
|
|
assert.True(t, errors.Is(joined, err1))
|
|
|
|
|
assert.True(t, errors.Is(joined, err2))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- Helper Function Tests ---
|
|
|
|
|
|
|
|
|
|
func TestOp_Good(t *testing.T) {
|
|
|
|
|
err := E("mypackage.MyFunc", "failed", errors.New("cause"))
|
|
|
|
|
assert.Equal(t, "mypackage.MyFunc", Op(err))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestOp_Good_NotLogError(t *testing.T) {
|
|
|
|
|
err := errors.New("plain error")
|
|
|
|
|
assert.Equal(t, "", Op(err))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestErrCode_Good(t *testing.T) {
|
|
|
|
|
err := WrapCode(errors.New("base"), "ERR_CODE", "op", "msg")
|
|
|
|
|
assert.Equal(t, "ERR_CODE", ErrCode(err))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestErrCode_Good_NoCode(t *testing.T) {
|
|
|
|
|
err := E("op", "msg", errors.New("base"))
|
|
|
|
|
assert.Equal(t, "", ErrCode(err))
|
|
|
|
|
}
|
|
|
|
|
|
fix(dx): update CLAUDE.md and raise test coverage to 97.7%
- CLAUDE.md: add missing exported symbols (NewError, Message, AllOps,
FormatStackTrace, Security, Username) to architecture description
- Add tests for SetOutput, SetRedactKeys, odd keyval handling,
op/stack deduplication, RotationWriterFactory, default output,
Username, and all package-level proxy functions
- Add ErrCode tests for plain error and nil inputs
- Coverage: 86.1% → 97.7%
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-17 07:18:38 +00:00
|
|
|
func TestErrCode_Good_PlainError(t *testing.T) {
|
|
|
|
|
err := errors.New("plain error")
|
|
|
|
|
assert.Equal(t, "", ErrCode(err))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestErrCode_Good_Nil(t *testing.T) {
|
|
|
|
|
assert.Equal(t, "", ErrCode(nil))
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-30 06:33:30 +00:00
|
|
|
func TestRetryAfter_Good(t *testing.T) {
|
|
|
|
|
retryAfter := 42 * time.Second
|
|
|
|
|
err := &Err{Msg: "typed", RetryAfter: &retryAfter}
|
|
|
|
|
|
|
|
|
|
got, ok := RetryAfter(err)
|
|
|
|
|
assert.True(t, ok)
|
|
|
|
|
assert.Equal(t, retryAfter, *got)
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-01 04:41:48 +00:00
|
|
|
func TestRetryAfter_Good_NestedChain(t *testing.T) {
|
|
|
|
|
retryAfter := 42 * time.Second
|
|
|
|
|
inner := &Err{Msg: "typed", RetryAfter: &retryAfter}
|
|
|
|
|
outer := &Err{Msg: "outer", Err: inner}
|
|
|
|
|
|
|
|
|
|
got, ok := RetryAfter(outer)
|
|
|
|
|
assert.True(t, ok)
|
|
|
|
|
assert.Equal(t, retryAfter, *got)
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-30 06:33:30 +00:00
|
|
|
func TestIsRetryable_Good(t *testing.T) {
|
|
|
|
|
err := &Err{Msg: "typed", Retryable: true}
|
|
|
|
|
assert.True(t, IsRetryable(err))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestRecoveryAction_Good(t *testing.T) {
|
|
|
|
|
err := &Err{Msg: "typed", NextAction: "inspect"}
|
|
|
|
|
assert.Equal(t, "inspect", RecoveryAction(err))
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-01 04:41:48 +00:00
|
|
|
func TestRecoveryAction_Good_NestedChain(t *testing.T) {
|
|
|
|
|
inner := &Err{Msg: "typed", NextAction: "inspect"}
|
|
|
|
|
outer := &Err{Msg: "outer", Err: inner}
|
|
|
|
|
|
|
|
|
|
assert.Equal(t, "inspect", RecoveryAction(outer))
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-06 09:30:57 +00:00
|
|
|
func TestMessage_Good(t *testing.T) {
|
|
|
|
|
err := E("op", "the message", errors.New("base"))
|
|
|
|
|
assert.Equal(t, "the message", Message(err))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestMessage_Good_PlainError(t *testing.T) {
|
|
|
|
|
err := errors.New("plain message")
|
|
|
|
|
assert.Equal(t, "plain message", Message(err))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestMessage_Good_Nil(t *testing.T) {
|
|
|
|
|
assert.Equal(t, "", Message(nil))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestRoot_Good(t *testing.T) {
|
|
|
|
|
root := errors.New("root cause")
|
|
|
|
|
level1 := Wrap(root, "level1", "wrapped once")
|
|
|
|
|
level2 := Wrap(level1, "level2", "wrapped twice")
|
|
|
|
|
|
|
|
|
|
assert.Equal(t, root, Root(level2))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestRoot_Good_SingleError(t *testing.T) {
|
|
|
|
|
err := errors.New("single")
|
|
|
|
|
assert.Equal(t, err, Root(err))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestRoot_Good_Nil(t *testing.T) {
|
|
|
|
|
assert.Nil(t, Root(nil))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- Log-and-Return Helper Tests ---
|
|
|
|
|
|
|
|
|
|
func TestLogError_Good(t *testing.T) {
|
|
|
|
|
// Capture log output
|
|
|
|
|
var buf bytes.Buffer
|
|
|
|
|
logger := New(Options{Level: LevelDebug, Output: &buf})
|
|
|
|
|
SetDefault(logger)
|
|
|
|
|
defer SetDefault(New(Options{Level: LevelInfo}))
|
|
|
|
|
|
|
|
|
|
underlying := errors.New("connection failed")
|
|
|
|
|
err := LogError(underlying, "db.Connect", "database unavailable")
|
|
|
|
|
|
|
|
|
|
// Check returned error
|
|
|
|
|
assert.NotNil(t, err)
|
|
|
|
|
assert.Contains(t, err.Error(), "db.Connect")
|
|
|
|
|
assert.Contains(t, err.Error(), "database unavailable")
|
|
|
|
|
assert.True(t, errors.Is(err, underlying))
|
|
|
|
|
|
|
|
|
|
// Check log output
|
|
|
|
|
output := buf.String()
|
|
|
|
|
assert.Contains(t, output, "[ERR]")
|
|
|
|
|
assert.Contains(t, output, "database unavailable")
|
2026-03-09 08:29:57 +00:00
|
|
|
assert.Contains(t, output, "op=\"db.Connect\"")
|
2026-03-06 09:30:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestLogError_Good_NilError(t *testing.T) {
|
|
|
|
|
var buf bytes.Buffer
|
|
|
|
|
logger := New(Options{Level: LevelDebug, Output: &buf})
|
|
|
|
|
SetDefault(logger)
|
|
|
|
|
defer SetDefault(New(Options{Level: LevelInfo}))
|
|
|
|
|
|
|
|
|
|
err := LogError(nil, "op", "msg")
|
|
|
|
|
assert.Nil(t, err)
|
|
|
|
|
assert.Empty(t, buf.String()) // No log output for nil error
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestLogWarn_Good(t *testing.T) {
|
|
|
|
|
var buf bytes.Buffer
|
|
|
|
|
logger := New(Options{Level: LevelDebug, Output: &buf})
|
|
|
|
|
SetDefault(logger)
|
|
|
|
|
defer SetDefault(New(Options{Level: LevelInfo}))
|
|
|
|
|
|
|
|
|
|
underlying := errors.New("cache miss")
|
|
|
|
|
err := LogWarn(underlying, "cache.Get", "falling back to db")
|
|
|
|
|
|
|
|
|
|
assert.NotNil(t, err)
|
|
|
|
|
assert.True(t, errors.Is(err, underlying))
|
|
|
|
|
|
|
|
|
|
output := buf.String()
|
|
|
|
|
assert.Contains(t, output, "[WRN]")
|
|
|
|
|
assert.Contains(t, output, "falling back to db")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestLogWarn_Good_NilError(t *testing.T) {
|
|
|
|
|
var buf bytes.Buffer
|
|
|
|
|
logger := New(Options{Level: LevelDebug, Output: &buf})
|
|
|
|
|
SetDefault(logger)
|
|
|
|
|
defer SetDefault(New(Options{Level: LevelInfo}))
|
|
|
|
|
|
|
|
|
|
err := LogWarn(nil, "op", "msg")
|
|
|
|
|
assert.Nil(t, err)
|
|
|
|
|
assert.Empty(t, buf.String())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestMust_Good_NoError(t *testing.T) {
|
|
|
|
|
// Should not panic when error is nil
|
|
|
|
|
assert.NotPanics(t, func() {
|
|
|
|
|
Must(nil, "test", "should not panic")
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestMust_Ugly_Panics(t *testing.T) {
|
|
|
|
|
var buf bytes.Buffer
|
|
|
|
|
logger := New(Options{Level: LevelDebug, Output: &buf})
|
|
|
|
|
SetDefault(logger)
|
|
|
|
|
defer SetDefault(New(Options{Level: LevelInfo}))
|
|
|
|
|
|
|
|
|
|
assert.Panics(t, func() {
|
|
|
|
|
Must(errors.New("fatal error"), "startup", "initialization failed")
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Verify error was logged before panic
|
|
|
|
|
output := buf.String()
|
|
|
|
|
assert.True(t, strings.Contains(output, "[ERR]") || len(output) > 0)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestStackTrace_Good(t *testing.T) {
|
|
|
|
|
// Nested operations
|
|
|
|
|
err := E("op1", "msg1", nil)
|
|
|
|
|
err = Wrap(err, "op2", "msg2")
|
|
|
|
|
err = Wrap(err, "op3", "msg3")
|
|
|
|
|
|
|
|
|
|
stack := StackTrace(err)
|
|
|
|
|
assert.Equal(t, []string{"op3", "op2", "op1"}, stack)
|
|
|
|
|
|
|
|
|
|
// Format
|
|
|
|
|
formatted := FormatStackTrace(err)
|
|
|
|
|
assert.Equal(t, "op3 -> op2 -> op1", formatted)
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-30 00:36:24 +00:00
|
|
|
func TestStackTrace_Bad_PlainError(t *testing.T) {
|
2026-03-06 09:30:57 +00:00
|
|
|
err := errors.New("plain error")
|
|
|
|
|
assert.Empty(t, StackTrace(err))
|
|
|
|
|
assert.Empty(t, FormatStackTrace(err))
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-30 00:36:24 +00:00
|
|
|
func TestStackTrace_Bad_Nil(t *testing.T) {
|
2026-03-06 09:30:57 +00:00
|
|
|
assert.Empty(t, StackTrace(nil))
|
|
|
|
|
assert.Empty(t, FormatStackTrace(nil))
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-30 00:36:24 +00:00
|
|
|
func TestStackTrace_Bad_NoOp(t *testing.T) {
|
2026-03-06 09:30:57 +00:00
|
|
|
err := &Err{Msg: "no op"}
|
|
|
|
|
assert.Empty(t, StackTrace(err))
|
|
|
|
|
assert.Empty(t, FormatStackTrace(err))
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-15 17:39:44 +00:00
|
|
|
func TestStackTrace_Mixed_Good(t *testing.T) {
|
2026-03-06 09:30:57 +00:00
|
|
|
err := E("inner", "msg", nil)
|
|
|
|
|
err = fmt.Errorf("wrapper: %w", err)
|
|
|
|
|
err = Wrap(err, "outer", "msg")
|
|
|
|
|
|
|
|
|
|
stack := StackTrace(err)
|
|
|
|
|
assert.Equal(t, []string{"outer", "inner"}, stack)
|
|
|
|
|
}
|