Files
summercms/modules/diary/console_test.go
Jakub Zych 24f4643283 test(diary): config, channels, rotation, redaction and slogtest
- config rules name their key; env strings overlay defaults; D-08 defaults
- component floors, verbosity 1-3, quiet on the console only
- daily rotation, retention, 0640 files, unwritable dir, lazy open, path confinement
- redaction on every driver, console control-character stripping and prefix
- loggers before Setup, Reset, HideOnTTY, 100 concurrent writers
- testing/slogtest for the console handler and the JSON pipeline
- sunscreen InstallDefault still installs a redacting logfmt handler
2026-10-21 19:09:28 +02:00

160 lines
6.1 KiB
Go

package diary_test
import (
"bytes"
"context"
"log/slog"
"strings"
"testing"
"time"
"git.golem15.com/golem15/summercms/modules/bonfire"
"git.golem15.com/golem15/summercms/modules/diary"
"github.com/charmbracelet/colorprofile"
)
// record handles one record with a zero time (no time column) on h.
func record(t *testing.T, h slog.Handler, level slog.Level, msg string, attrs ...slog.Attr) {
t.Helper()
r := slog.NewRecord(time.Time{}, level, msg, 0)
r.AddAttrs(attrs...)
if err := h.Handle(context.Background(), r); err != nil {
t.Fatalf("Handle: %v", err)
}
}
// TestConsoleStripsControlChars: control characters in the message, in
// attribute keys and in values never reach the terminal; line breaks become
// spaces, so one record is one line.
func TestConsoleStripsControlChars(t *testing.T) {
for _, profile := range []colorprofile.Profile{colorprofile.NoTTY, colorprofile.ANSI256} {
var buf bytes.Buffer
h := diary.NewConsoleHandler(&buf, diary.ConsoleOptions{Profile: profile})
record(t, h, slog.LevelInfo, "evil \x1b[31mred\x1b[0m\a bell\nsecond line\rreturn",
slog.String("value", "a\x1b]0;title\x07b\nc"),
slog.String("k\x1bey", "v"),
slog.Any("err", errString("boom\x1b[2Jclear")))
out := buf.String()
if strings.Count(out, "\n") != 1 || !strings.HasSuffix(out, "\n") {
t.Errorf("profile %v: record is not exactly one line: %q", profile, out)
}
for _, bad := range []string{"\x1b[31mred", "\x1b]0;", "\a", "\x07", "\r", "\x1b[2J", "k\x1bey"} {
if strings.Contains(out, bad) {
t.Errorf("profile %v: output carries %q: %q", profile, bad, out)
}
}
for _, want := range []string{"[31mred", "bell second line return", "[2Jclear", "key="} {
if !strings.Contains(out, want) {
t.Errorf("profile %v: output lacks the cleaned text %q: %q", profile, want, out)
}
}
}
}
type errString string
func (e errString) Error() string { return string(e) }
// TestConsoleComponentPrefix: the component is a fixed-width column, so
// messages line up whether a record has a short component, none or one
// given as a record attribute; inside a group it is an ordinary attribute.
func TestConsoleComponentPrefix(t *testing.T) {
var buf bytes.Buffer
h := diary.NewConsoleHandler(&buf, diary.ConsoleOptions{Profile: colorprofile.NoTTY})
record(t, h.WithAttrs([]slog.Attr{slog.String(diary.ComponentKey, "db")}), slog.LevelInfo, "MSG")
record(t, h.WithAttrs([]slog.Attr{slog.String(diary.ComponentKey, "journal")}), slog.LevelWarn, "MSG")
record(t, h, slog.LevelError, "MSG")
record(t, h, slog.LevelDebug, "MSG", slog.String(diary.ComponentKey, "river"))
record(t, h.WithGroup("g").WithAttrs([]slog.Attr{slog.String(diary.ComponentKey, "inner")}), slog.LevelInfo, "MSG")
lines := strings.Split(strings.TrimSuffix(buf.String(), "\n"), "\n")
if len(lines) != 5 {
t.Fatalf("got %d lines, want 5:\n%s", len(lines), buf.String())
}
wantPrefix := []string{
"INFO db MSG",
"WARN journal MSG",
"ERROR MSG",
"DEBUG river MSG",
"INFO MSG g.component=inner",
}
for i, want := range wantPrefix {
if !strings.HasPrefix(lines[i], want) {
t.Errorf("line %d = %q, want prefix %q", i, lines[i], want)
}
if col := strings.Index(lines[i], "MSG"); col != 17 {
t.Errorf("line %d: message at column %d, want 17: %q", i, col, lines[i])
}
}
if strings.Contains(lines[3], "component=") {
t.Errorf("record-level component printed as an attribute: %q", lines[3])
}
// A record with a time starts with HH:MM:SS.
buf.Reset()
r := slog.NewRecord(time.Date(2026, 10, 21, 9, 5, 7, 0, time.Local), slog.LevelInfo, "timed", 0)
if err := h.Handle(context.Background(), r); err != nil {
t.Fatal(err)
}
if !strings.HasPrefix(buf.String(), "09:05:07 INFO ") {
t.Errorf("timed line = %q, want the 09:05:07 prefix", buf.String())
}
}
// TestConsoleStyledOnlyOnTerminal: the NoTTY profile writes no escape
// codes and ANSI256 does; through Setup, the auto format styles only when
// the profile allows it (never on a plain buffer unless forced).
func TestConsoleStyledOnlyOnTerminal(t *testing.T) {
for _, tc := range []struct {
profile colorprofile.Profile
styled bool
}{{colorprofile.NoTTY, false}, {colorprofile.ANSI256, true}, {colorprofile.TrueColor, true}} {
var buf bytes.Buffer
h := diary.NewConsoleHandler(&buf, diary.ConsoleOptions{Profile: tc.profile})
record(t, h.WithAttrs([]slog.Attr{slog.String(diary.ComponentKey, "blog")}), slog.LevelWarn, "styled?", slog.Int("n", 1))
if got := strings.Contains(buf.String(), "\x1b["); got != tc.styled {
t.Errorf("profile %v: escape codes %v, want %v: %q", tc.profile, got, tc.styled, buf.String())
}
}
for _, tc := range []struct {
name string
color bonfire.ColorMode
environ []string
styled bool
}{
{"auto on a buffer", bonfire.ColorAuto, []string{}, false},
{"auto forced", bonfire.ColorAuto, []string{"FORCE_COLOR=1"}, true},
{"auto forced but NO_COLOR", bonfire.ColorAuto, []string{"FORCE_COLOR=1", "NO_COLOR=1"}, false},
{"always", bonfire.ColorAlways, []string{}, true},
{"never forced", bonfire.ColorNever, []string{"FORCE_COLOR=1"}, false},
} {
t.Run(tc.name, func(t *testing.T) {
var buf lockedBuffer
setup(t, diary.Options{Stderr: &buf, Color: tc.color, Environ: tc.environ}, "SUMMER_LOG__DEFAULT=console")
diary.For("blog").Info("hello")
out := buf.String()
if got := strings.Contains(out, "\x1b["); got != tc.styled {
t.Errorf("escape codes %v, want %v: %q", got, tc.styled, out)
}
if !tc.styled && !strings.Contains(out, "level=INFO") {
t.Errorf("unstyled console is not logfmt: %q", out)
}
})
}
}
// TestConsoleLevelFromOptions: ConsoleOptions.Level gates records and nil
// means info.
func TestConsoleLevelFromOptions(t *testing.T) {
var buf bytes.Buffer
h := diary.NewConsoleHandler(&buf, diary.ConsoleOptions{Profile: colorprofile.NoTTY})
if h.Enabled(context.Background(), slog.LevelDebug) || !h.Enabled(context.Background(), slog.LevelInfo) {
t.Error("default console level is not info")
}
h = diary.NewConsoleHandler(&buf, diary.ConsoleOptions{Profile: colorprofile.NoTTY, Level: slog.LevelError})
if h.Enabled(context.Background(), slog.LevelWarn) {
t.Error("console level error lets warn through")
}
}