package logfield_test import ( "bytes" "log/slog" "strings" "testing" "unicode/utf8" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "sneak.berlin/go/webhooker/internal/logfield" ) // encodedCost is what a whole string costs on a line, by the same // accounting Truncate spends its budget with. func encodedCost(s string) int { total := 0 for _, r := range s { total += logfield.EncodedBytes(r) } return total } // TestTruncate_SpendsEncodedBytesNotRawBytes is the zero-headroom // version of the line-length assertions elsewhere. // // A line ceiling has slack in it by construction, so a line-level // assertion only catches a raw-byte budget for the fills with the // widest multiplier. Here the budget is checked against exactly what // it bought: a value built from a single rune must keep exactly // MaxBytes/EncodedBytes(r) of them, for every rune, with nothing // spare. func TestTruncate_SpendsEncodedBytesNotRawBytes(t *testing.T) { t.Parallel() for name, r := range map[string]rune{ "plain": 'x', "quote": '"', "backslash": '\\', "tab": '\t', "newline": '\n', "carriage_return": '\r', "c0_control": '', "del": '', "line_separator": '
', "astral_nonprintable": '\U0001000C', "multibyte_printable": 'é', "three_byte_printable": '€', "emoji_printable": '\U0001F600', } { t.Run(name, func(t *testing.T) { t.Parallel() cost := logfield.EncodedBytes(r) want := logfield.MaxBytes / cost // Far past the budget under either accounting. in := strings.Repeat(string(r), logfield.MaxBytes*2) got := logfield.Truncate(in, logfield.MaxBytes) assert.True( t, strings.HasSuffix(got, "[truncated]"), "a value past the budget must be marked", ) kept := strings.TrimSuffix(got, "[truncated]") assert.Equal( t, want, utf8.RuneCountInString(kept), "budget bought the wrong number of runes at "+ "%d encoded bytes each", cost, ) assert.LessOrEqual( t, encodedCost(kept), logfield.MaxBytes, ) }) } } // TestEncodedBytes_CoversWhatTheHandlersActuallyEmit measures the // charge against what slog really writes rather than against a // reading of its source, over both handlers internal/logger can // install. An undercharged rune fails here rather than quietly // falsifying every stated line ceiling. func TestEncodedBytes_CoversWhatTheHandlersActuallyEmit(t *testing.T) { t.Parallel() emitted := func(h func(*bytes.Buffer) slog.Handler, r rune) int { var withValue, withoutValue bytes.Buffer slog.New(h(&withValue)).Info("m", "v", string(r)) slog.New(h(&withoutValue)).Info("m", "v", "") return withValue.Len() - withoutValue.Len() } jsonHandler := func(b *bytes.Buffer) slog.Handler { return slog.NewJSONHandler(b, &slog.HandlerOptions{ ReplaceAttr: dropTime, }) } textHandler := func(b *bytes.Buffer) slog.Handler { return slog.NewTextHandler(b, &slog.HandlerOptions{ ReplaceAttr: dropTime, }) } // Every code point below U+0800 densely — which covers all of C0, // DEL, C1 and the two-byte range — plus the separators only the // JSON handler escapes, plus a stratified walk across the rest of // the assigned space and into the astral planes. var runes []rune for r := rune(1); r < 0x800; r++ { runes = append(runes, r) } runes = append( runes, '
', // LINE SEPARATOR, escaped only by the JSON handler '
', // PARAGRAPH SEPARATOR, likewise rune(0xFEFF), // ZERO WIDTH NO-BREAK SPACE rune(0xFFFD), // REPLACEMENT CHARACTER ) for r := rune(0x800); r <= 0x10FFFF; r += 0x1D1 { runes = append(runes, r) } for _, r := range runes { if !utf8.ValidRune(r) { continue } charged := logfield.EncodedBytes(r) require.LessOrEqual( t, emitted(jsonHandler, r), charged, "json handler spends more than U+%04X is charged", r, ) require.LessOrEqual( t, emitted(textHandler, r), charged, "text handler spends more than U+%04X is charged", r, ) } } func dropTime(_ []string, a slog.Attr) slog.Attr { if a.Key == slog.TimeKey { return slog.Attr{} } return a } // TestTruncate_LeavesShortValuesAlone keeps the marker meaningful: a // value that fits is returned untouched, so a reader can tell a short // value from a cut one. func TestTruncate_LeavesShortValuesAlone(t *testing.T) { t.Parallel() const in = "Mozilla/5.0 (X11; Linux x86_64)" assert.Equal(t, in, logfield.Truncate(in, logfield.MaxBytes)) } // TestTruncate_DropsInvalidUTF8 covers a value that was never valid // UTF-8 — a SQL parameter or a header can carry one. Replacing each // bad byte would cost six encoded bytes apiece, so they are dropped. func TestTruncate_DropsInvalidUTF8(t *testing.T) { t.Parallel() got := logfield.Truncate("a\xffb\xfec", logfield.MaxBytes) assert.Equal(t, "abc", got) assert.True(t, utf8.ValidString(got)) } // TestTruncate_NeverSplitsARune covers a cut landing inside a // multi-byte encoding. func TestTruncate_NeverSplitsARune(t *testing.T) { t.Parallel() // A three-byte printable rune, so a small budget lands inside the // encoding rather than between two of them. in := strings.Repeat("€", logfield.MaxBytes) for budget := 1; budget <= 16; budget++ { got := strings.TrimSuffix( logfield.Truncate(in, budget), "[truncated]", ) assert.True( t, utf8.ValidString(got), "budget %d produced invalid UTF-8", budget, ) assert.LessOrEqual(t, encodedCost(got), budget) } }