v0.1.8 tests converted to standard library from testify

This commit is contained in:
2026-07-24 13:48:21 -04:00
parent 5553aeaec4
commit 24b7deebdb
19 changed files with 2399 additions and 1795 deletions
+287 -164
View File
@@ -4,220 +4,324 @@ import (
"bytes"
"encoding/json"
"errors"
"reflect"
"strings"
"sync"
"testing"
"time"
"github.com/lixenwraith/log/sanitizer"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestFormatter(t *testing.T) {
timestamp := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC)
t.Run("fluent API", func(t *testing.T) {
s := sanitizer.New().Policy(sanitizer.PolicyRaw)
f := New(s).
Type("json").
TimestampFormat(time.RFC3339).
ShowLevel(true).
ShowTimestamp(true)
data := f.Format(0, timestamp, 0, "", []any{"test"})
assert.Contains(t, string(data), `"level":"INFO"`)
assert.Contains(t, string(data), `"time":"2024-01-01T12:00:00Z"`)
})
t.Run("txt format", func(t *testing.T) {
s := sanitizer.New().Policy(sanitizer.PolicyRaw)
f := New(s).Type("txt")
data := f.Format(FlagDefault, timestamp, 0, "", []any{"test message", 123})
str := string(data)
assert.Contains(t, str, "2024-01-01")
assert.Contains(t, str, "INFO")
assert.Contains(t, str, "test message")
assert.Contains(t, str, "123")
assert.True(t, strings.HasSuffix(str, "\n"))
})
t.Run("json format", func(t *testing.T) {
s := sanitizer.New().Policy(sanitizer.PolicyRaw)
f := New(s).Type("json")
data := f.Format(FlagDefault, timestamp, 4, "trace1", []any{"warning", true})
var result map[string]any
err := json.Unmarshal(data[:len(data)-1], &result) // Remove trailing newline
require.NoError(t, err)
assert.Equal(t, "WARN", result["level"])
assert.Equal(t, "trace1", result["trace"])
fields := result["fields"].([]any)
assert.Equal(t, "warning", fields[0])
assert.Equal(t, true, fields[1])
})
t.Run("raw format", func(t *testing.T) {
s := sanitizer.New().Policy(sanitizer.PolicyRaw)
f := New(s).Type("raw")
data := f.FormatWithOptions("raw", 0, timestamp, 0, "", []any{"raw", "data", 42})
str := string(data)
assert.Equal(t, "raw data 42", str)
assert.False(t, strings.HasSuffix(str, "\n"))
})
t.Run("flag override raw", func(t *testing.T) {
s := sanitizer.New().Policy(sanitizer.PolicyRaw)
f := New(s).Type("json") // Configure as JSON
data := f.Format(FlagRaw, timestamp, 0, "", []any{"forced", "raw"})
str := string(data)
assert.Equal(t, "forced raw", str)
})
t.Run("structured json", func(t *testing.T) {
s := sanitizer.New().Policy(sanitizer.PolicyJSON)
f := New(s).Type("json")
fields := map[string]any{"key1": "value1", "key2": 42}
data := f.Format(FlagStructuredJSON|FlagDefault, timestamp, 0, "",
[]any{"structured message", fields})
var result map[string]any
err := json.Unmarshal(data[:len(data)-1], &result)
require.NoError(t, err)
assert.Equal(t, "structured message", result["message"])
assert.Equal(t, map[string]any{"key1": "value1", "key2": float64(42)}, result["fields"])
})
t.Run("special characters escaping", func(t *testing.T) {
// PolicyRaw — transport escaping applies exactly once.
// PolicyJSON + json format double-escapes (see TestJSONSanitizerLayering).
s := sanitizer.New().Policy(sanitizer.PolicyRaw)
f := New(s).Type("json")
data := f.Format(FlagDefault, timestamp, 0, "",
[]any{"test\n\r\t\"\\message"})
str := string(data)
assert.Contains(t, str, `test\n\r\t\"\\message`)
})
t.Run("error type handling", func(t *testing.T) {
s := sanitizer.New().Policy(sanitizer.PolicyRaw)
f := New(s).Type("txt")
err := errors.New("test error")
data := f.Format(FlagDefault, timestamp, 8, "", []any{err})
str := string(data)
assert.Contains(t, str, "test error")
})
func eq[T comparable](tb testing.TB, got, want T, ctx string) {
tb.Helper()
if got != want {
tb.Errorf("%s: got %#v, want %#v", ctx, got, want)
}
}
func TestJSONUTF8Passthrough(t *testing.T) {
timestamp := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC)
f := New(sanitizer.New()).Type("json")
in := "héllo 世界 ✓"
data := f.Format(FlagDefault, timestamp, 0, "", []any{in})
func contains(tb testing.TB, haystack, needle, ctx string) {
tb.Helper()
if !strings.Contains(haystack, needle) {
tb.Errorf("%s: %q not found in %q", ctx, needle, haystack)
}
}
func notContains(tb testing.TB, haystack, needle, ctx string) {
tb.Helper()
if strings.Contains(haystack, needle) {
tb.Errorf("%s: %q unexpectedly present in %q", ctx, needle, haystack)
}
}
func mustNoErr(tb testing.TB, err error, ctx string) {
tb.Helper()
if err != nil {
tb.Fatalf("%s: unexpected error: %v", ctx, err)
}
}
// unmarshalRecord parses one json record, stripping the trailing newline.
func unmarshalRecord(tb testing.TB, data []byte) map[string]any {
tb.Helper()
var result map[string]any
require.NoError(t, json.Unmarshal(bytes.TrimSuffix(data, []byte("\n")), &result))
assert.Equal(t, in, result["fields"].([]any)[0])
assert.NotContains(t, string(data), `\u00`, "no per-byte escapes of UTF-8")
if err := json.Unmarshal(bytes.TrimSuffix(data, []byte("\n")), &result); err != nil {
tb.Fatalf("parse record %q: %v", data, err)
}
return result
}
func TestJSONSanitizerLayering(t *testing.T) {
timestamp := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC)
var testStamp = time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC)
// Content transform (PolicyTxt) applied before transport escaping
type stringerValue struct{}
func (stringerValue) String() string { return "stringer" }
func TestFormatTxt(t *testing.T) {
f := New(sanitizer.New().Policy(sanitizer.PolicyRaw)).Type("txt")
str := string(f.Format(FlagDefault, testStamp, 0, "", []any{"test message", 123}))
contains(t, str, "2024-01-01", "timestamp")
contains(t, str, "INFO", "level")
contains(t, str, `"test message"`, "quoted argument")
contains(t, str, "123", "numeric argument")
if !strings.HasSuffix(str, "\n") {
t.Error("txt records must be newline terminated")
}
}
func TestFormatJSON(t *testing.T) {
f := New(sanitizer.New().Policy(sanitizer.PolicyRaw)).Type("json")
result := unmarshalRecord(t, f.Format(FlagDefault, testStamp, 4, "trace1", []any{"warning", true}))
eq(t, result["level"], any("WARN"), "level")
eq(t, result["trace"], any("trace1"), "trace")
fields := result["fields"].([]any)
eq(t, fields[0], any("warning"), "field 0")
eq(t, fields[1], any(true), "field 1")
}
func TestFormatFluentConfiguration(t *testing.T) {
f := New(sanitizer.New()).
Type("json").
TimestampFormat(time.RFC3339).
ShowLevel(true).
ShowTimestamp(true)
str := string(f.Format(0, testStamp, 0, "", []any{"test"}))
contains(t, str, `"level":"INFO"`, "configured level display")
contains(t, str, `"time":"2024-01-01T12:00:00Z"`, "configured timestamp format")
// An empty format string leaves the previous value in place
f.TimestampFormat("")
contains(t, string(f.Format(0, testStamp, 0, "", []any{"test"})),
`"time":"2024-01-01T12:00:00Z"`, "empty format ignored")
}
func TestFormatRaw(t *testing.T) {
f := New(sanitizer.New().Policy(sanitizer.PolicyRaw)).Type("raw")
str := string(f.FormatWithOptions("raw", 0, testStamp, 0, "", []any{"raw", "data", 42}))
eq(t, str, "raw data 42", "space-joined values")
if strings.HasSuffix(str, "\n") {
t.Error("raw records must not be newline terminated")
}
}
func TestFlagRawBypass(t *testing.T) {
// FlagRaw bypasses both the configured format and the sanitizer
f := New(sanitizer.New().Policy(sanitizer.PolicyTxt)).Type("json")
data := f.Format(FlagDefault, timestamp, 0, "", []any{"a\x07b"})
var result map[string]any
require.NoError(t, json.Unmarshal(bytes.TrimSuffix(data, []byte("\n")), &result))
assert.Equal(t, "a<07>b", result["fields"].([]any)[0])
// PolicyJSON + json format: content transform emits literal backslash
// sequences; transport escaping preserves them (double-escape by design)
f2 := New(sanitizer.New().Policy(sanitizer.PolicyJSON)).Type("json")
data2 := f2.Format(FlagDefault, timestamp, 0, "", []any{"a\nb"})
require.NoError(t, json.Unmarshal(bytes.TrimSuffix(data2, []byte("\n")), &result))
assert.Equal(t, `a\nb`, result["fields"].([]any)[0])
eq(t, string(f.Format(FlagRaw, testStamp, 0, "", []any{"forced", "raw"})),
"forced raw", "format bypass")
eq(t, string(f.Format(FlagRaw, testStamp, 0, "", []any{"esc\x1b[31m"})),
"esc\x1b[31m", "sanitizer bypass")
eq(t, string(f.Format(FlagRaw, testStamp, 0, "", []any{
[]byte("bytes"), stringerValue{}, errors.New("boom"), 7,
})), "bytes stringer boom 7", "type handling under FlagRaw")
}
func TestStructuredJSON(t *testing.T) {
f := New(sanitizer.New().Policy(sanitizer.PolicyJSON)).Type("json")
fields := map[string]any{"key1": "value1", "key2": 42}
result := unmarshalRecord(t, f.Format(FlagStructuredJSON|FlagDefault, testStamp, 0, "",
[]any{"structured message", fields}))
eq(t, result["message"], any("structured message"), "message key")
want := map[string]any{"key1": "value1", "key2": float64(42)}
if !reflect.DeepEqual(result["fields"], want) {
t.Errorf("fields: got %#v, want %#v", result["fields"], want)
}
// The structured branch requires two arguments; otherwise output falls back
// to the positional fields array
result = unmarshalRecord(t, f.Format(FlagStructuredJSON|FlagDefault, testStamp, 0, "",
[]any{"only a message"}))
if _, ok := result["message"]; ok {
t.Error("structured branch must not fire with a single argument")
}
if _, ok := result["fields"].([]any); !ok {
t.Errorf("expected positional fields array, got %#v", result["fields"])
}
}
func TestJSONEscaping(t *testing.T) {
t.Run("transport escaping applied once under PolicyRaw", func(t *testing.T) {
f := New(sanitizer.New().Policy(sanitizer.PolicyRaw)).Type("json")
str := string(f.Format(FlagDefault, testStamp, 0, "", []any{"test\n\r\t\"\\message"}))
contains(t, str, `test\n\r\t\"\\message`, "escapes")
})
t.Run("UTF-8 passthrough", func(t *testing.T) {
f := New(sanitizer.New()).Type("json")
in := "héllo 世界 ✓"
data := f.Format(FlagDefault, testStamp, 0, "", []any{in})
result := unmarshalRecord(t, data)
eq(t, result["fields"].([]any)[0], any(in), "round trip")
notContains(t, string(data), `\u00`, "no per-byte escapes of UTF-8")
})
t.Run("content transform precedes transport escaping", func(t *testing.T) {
f := New(sanitizer.New().Policy(sanitizer.PolicyTxt)).Type("json")
result := unmarshalRecord(t, f.Format(FlagDefault, testStamp, 0, "", []any{"a\x07b"}))
eq(t, result["fields"].([]any)[0], any("a<07>b"), "hex encoded before escaping")
})
t.Run("PolicyJSON double-escapes by design", func(t *testing.T) {
// The content transform emits literal backslash sequences that the
// transport layer then escapes again
f := New(sanitizer.New().Policy(sanitizer.PolicyJSON)).Type("json")
result := unmarshalRecord(t, f.Format(FlagDefault, testStamp, 0, "", []any{"a\nb"}))
eq(t, result["fields"].([]any)[0], any(`a\nb`), "double escape")
})
}
func TestTraceHandling(t *testing.T) {
t.Run("txt sanitizes and unquotes", func(t *testing.T) {
f := New(sanitizer.New().Policy(sanitizer.PolicyTxt)).
Type("txt").ShowTimestamp(false).ShowLevel(false)
// Control sequences in the trace must not reach a terminal verbatim
str := string(f.Format(0, testStamp, 0, "caller\x1b[31m", []any{"msg"}))
contains(t, str, "caller<1b>[31m", "sanitized trace")
notContains(t, str, "\x1b", "raw escape sequence")
if strings.HasPrefix(str, `"`) {
t.Errorf("trace must not retain serializer quotes: %q", str)
}
})
t.Run("empty trace is omitted", func(t *testing.T) {
f := New(sanitizer.New()).Type("json")
result := unmarshalRecord(t, f.Format(FlagDefault, testStamp, 0, "", []any{"msg"}))
if _, ok := result["trace"]; ok {
t.Error("empty trace must not emit a key")
}
})
}
func TestFlagResolution(t *testing.T) {
timestamp := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC)
f := New(sanitizer.New()).Type("txt").ShowTimestamp(true).ShowLevel(true)
// Non-display flags alone inherit configured defaults
str := string(f.Format(FlagStructuredJSON, timestamp, 0, "", []any{"m"}))
assert.Contains(t, str, "2024-01-01")
assert.Contains(t, str, "INFO")
// Non-display flags alone inherit the configured defaults
str := string(f.Format(FlagStructuredJSON, testStamp, 0, "", []any{"m"}))
contains(t, str, "2024-01-01", "inherited timestamp")
contains(t, str, "INFO", "inherited level")
// Explicit suppression
str = string(f.Format(FlagNoLevel, timestamp, 0, "", []any{"m"}))
assert.Contains(t, str, "2024-01-01")
assert.NotContains(t, str, "INFO")
str = string(f.Format(FlagNoLevel, testStamp, 0, "", []any{"m"}))
contains(t, str, "2024-01-01", "timestamp retained")
notContains(t, str, "INFO", "level suppressed")
str = string(f.Format(FlagNoTimestamp|FlagNoLevel, timestamp, 0, "", []any{"m"}))
assert.NotContains(t, str, "2024-01-01")
assert.NotContains(t, str, "INFO")
str = string(f.Format(FlagNoTimestamp|FlagNoLevel, testStamp, 0, "", []any{"m"}))
notContains(t, str, "2024-01-01", "timestamp suppressed")
notContains(t, str, "INFO", "level suppressed")
// FlagNo* wins over FlagShow* on conflict
str = string(f.Format(FlagShowLevel|FlagNoLevel, testStamp, 0, "", []any{"m"}))
notContains(t, str, "INFO", "suppression precedence")
// Show flags override a disabled default
off := New(sanitizer.New()).Type("txt").ShowTimestamp(false).ShowLevel(false)
str = string(off.Format(FlagShowLevel, testStamp, 0, "", []any{"m"}))
contains(t, str, "INFO", "explicit enable")
// FormatWithOptions is fully explicit: unset Show bits mean off
str = string(f.FormatWithOptions("txt", 0, timestamp, 0, "", []any{"m"}))
assert.NotContains(t, str, "INFO")
str = string(f.FormatWithOptions("txt", 0, testStamp, 0, "", []any{"m"}))
notContains(t, str, "INFO", "explicit API ignores defaults")
}
func TestUnknownFormatFallback(t *testing.T) {
timestamp := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC)
f := New(sanitizer.New()).Type("txt")
data := f.FormatWithOptions("xml", FlagShowLevel, timestamp, 8, "", []any{"boom"})
require.NotNil(t, data)
assert.Contains(t, string(data), "ERROR")
assert.Contains(t, string(data), "boom")
data := f.FormatWithOptions("xml", FlagShowLevel, testStamp, 8, "", []any{"boom"})
if data == nil {
t.Fatal("unknown format returned nil")
}
contains(t, string(data), "ERROR", "level")
contains(t, string(data), "boom", "payload")
// The configured type is normalized on the value paths as well
unknown := New(sanitizer.New()).Type("xml")
eq(t, string(unknown.FormatArgs("a b")), `"a b"`, "normalized to txt")
}
func TestAppendValueTypes(t *testing.T) {
f := New(sanitizer.New()).Type("raw")
tests := []struct {
name string
in any
want string
}{
{"string", "text", "text"},
{"bytes", []byte("bytes"), "bytes"},
{"rune", 'A', "A"},
{"int", 42, "42"},
{"int64", int64(64), "64"},
{"uint", uint(7), "7"},
{"uint64", uint64(8), "8"},
{"float32", float32(1.5), "1.5"},
{"float64", 2.25, "2.25"},
{"bool", true, "true"},
{"nil", nil, "nil"},
{"time", testStamp, "2024-01-01T12:00:00Z"},
{"error", errors.New("boom"), "boom"},
{"stringer", stringerValue{}, "stringer"},
{"complex", map[string]int{"a": 1}, "map[a:1]"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
eq(t, string(f.FormatValue(tt.in)), tt.want, tt.name)
eq(t, string(f.AppendValue(nil, tt.in)), tt.want, tt.name+" append")
})
}
}
func TestFormatArgs(t *testing.T) {
f := New(sanitizer.New()).Type("raw")
eq(t, string(f.FormatArgs("a", 1, true)), "a 1 true", "space joined")
eq(t, string(f.FormatArgs()), "", "no arguments")
// Append variants extend a caller buffer without a leading separator
buf := append([]byte(nil), "prefix:"...)
eq(t, string(f.AppendArgs(buf, "a", "b")), "prefix:a b", "append args")
}
func TestReturnedSliceInvalidation(t *testing.T) {
timestamp := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC)
f := New(sanitizer.New()).Type("txt").ShowTimestamp(false).ShowLevel(false)
first := f.Format(0, timestamp, 0, "", []any{"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"})
first := f.Format(0, testStamp, 0, "", []any{"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"})
snapshot := string(first)
_ = f.Format(0, timestamp, 0, "", []any{"b"})
assert.NotEqual(t, snapshot, string(first),
"buffered Format output is invalidated by the next buffered call")
_ = f.Format(0, testStamp, 0, "", []any{"b"})
if snapshot == string(first) {
t.Error("buffered Format output must be invalidated by the next buffered call")
}
}
func TestAppendFormatStable(t *testing.T) {
timestamp := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC)
f := New(sanitizer.New()).Type("txt").ShowTimestamp(false).ShowLevel(false)
first := f.AppendFormat(nil, 0, timestamp, 0, "", []any{"first-payload"})
first := f.AppendFormat(nil, 0, testStamp, 0, "", []any{"first-payload"})
snapshot := string(first)
_ = f.Format(0, timestamp, 0, "", []any{"interleaved-buffered-call"})
second := f.AppendFormat(nil, 0, timestamp, 0, "", []any{"second"})
_ = f.Format(0, testStamp, 0, "", []any{"interleaved-buffered-call"})
second := f.AppendFormat(nil, 0, testStamp, 0, "", []any{"second"})
assert.Equal(t, snapshot, string(first), "caller-owned buffer unaffected by buffered calls")
assert.Equal(t, "second\n", string(second))
eq(t, string(first), snapshot, "caller-owned buffer unaffected by buffered calls")
eq(t, string(second), "second\n", "subsequent append")
}
func TestFormatterConcurrentAppend(t *testing.T) {
f := New(sanitizer.New().Policy(sanitizer.PolicyTxt)).Type("json")
var wg sync.WaitGroup
for i := 0; i < 16; i++ {
for i := range 16 {
wg.Add(1)
go func(id int) {
defer wg.Done()
for j := 0; j < 200; j++ {
out := f.AppendFormat(nil, FlagDefault, time.Now(), 0, "", []any{"w", id, "i", j, "s", "x\x00y"})
for j := range 200 {
out := f.AppendFormat(nil, FlagDefault, time.Now(), 0, "",
[]any{"w", id, "i", j, "s", "x\x00y"})
if !json.Valid(bytes.TrimSuffix(out, []byte("\n"))) {
t.Errorf("invalid JSON: %s", out)
return
@@ -241,11 +345,30 @@ func TestLevelToString(t *testing.T) {
{16, "DISK"},
{20, "SYS"},
{999, "LEVEL(999)"},
{-1, "LEVEL(-1)"},
}
for _, tt := range tests {
t.Run(tt.expected, func(t *testing.T) {
assert.Equal(t, tt.expected, LevelToString(tt.level))
eq(t, LevelToString(tt.level), tt.expected, "LevelToString")
})
}
}
func BenchmarkAppendFormat(b *testing.B) {
formats := []string{"txt", "json", "raw"}
args := []any{"request served", "status", 200, "client_ip", "127.0.0.1"}
for _, format := range formats {
b.Run(format, func(b *testing.B) {
f := New(sanitizer.New().Policy(sanitizer.PolicyTxt)).Type(format)
buf := make([]byte, 0, 512)
b.ReportAllocs()
for b.Loop() {
buf = f.AppendFormat(buf[:0], FlagDefault, testStamp, 0, "", args)
}
_ = buf
})
}
}