]> git.giacomolli.uk - onechat/blob - daemon_log_test.go
31a327878c485c60ea752cb1dfc4ec578b730146
[onechat] / daemon_log_test.go
1 package main
2
3 import (
4 "context"
5 "errors"
6 "io"
7 "log/slog"
8 "strings"
9 "sync"
10 "testing"
11 "time"
12
13 "github.com/stretchr/testify/require"
14 )
15
16 func TestDaemonLogFormatsLevelsAndPrefixes(t *testing.T) {
17 testCases := []struct {
18 name string
19 level slog.Level
20 prefix string
21 priority string
22 }{
23 {
24 name: "error names the failed part",
25 level: slog.LevelError,
26 prefix: "onechat: ",
27 priority: "3",
28 },
29 {
30 name: "warning names the retried part",
31 level: slog.LevelWarn,
32 prefix: "telegram: ",
33 priority: "4",
34 },
35 {
36 name: "info names the connected part",
37 level: slog.LevelInfo,
38 prefix: "whatsapp: ",
39 priority: "6",
40 },
41 {
42 name: "debug has its own priority",
43 level: slog.LevelDebug,
44 prefix: "onechat: ",
45 priority: "7",
46 },
47 }
48
49 for _, tc := range testCases {
50 t.Run(tc.name, func(t *testing.T) {
51 for _, journal := range []bool{false, true} {
52 var out strings.Builder
53
54 handler := &DaemonLogHandler{
55 out: &out,
56 prefix: tc.prefix,
57 journal: journal,
58 mutex: &sync.Mutex{},
59 }
60
61 record := slog.NewRecord(time.Date(2026, 1, 2, 3, 4, 5, 0, time.UTC), tc.level, "ready", 0)
62 require.True(t, handler.Enabled(t.Context(), tc.level))
63 require.NoError(t, handler.Handle(t.Context(), record))
64
65 expected := "2026/01/02 03:04:05 " + tc.level.String() + " " + tc.prefix + "ready\n"
66 if journal {
67 expected = "<" + tc.priority + ">" + tc.prefix + "ready\n"
68 }
69
70 require.Equal(t, expected, out.String())
71 }
72 })
73 }
74 }
75
76 func TestDaemonLogKeepsAttributesInOrderAndCauseLast(t *testing.T) {
77 var out strings.Builder
78
79 handler := &DaemonLogHandler{
80 out: &out,
81 prefix: "telegram: ",
82 journal: true,
83 mutex: &sync.Mutex{},
84 }
85
86 logger := slog.New(handler).With("first", "two words", slog.Attr{})
87 logger.Warn("receive failed", "err", errors.New("502 Bad Gateway"), "retry", 2040*time.Millisecond, "count", 3, "short", 1500*time.Microsecond)
88 require.Equal(t, "<4>telegram: receive failed first=two words retry=2s count=3 short=2ms: 502 Bad Gateway\n", out.String())
89 }
90
91 func TestDaemonLogOmitsEmptyAttributesAndNilCause(t *testing.T) {
92 var out strings.Builder
93
94 handler := &DaemonLogHandler{
95 out: &out,
96 prefix: "onechat: ",
97 journal: true,
98 mutex: &sync.Mutex{},
99 }
100
101 slog.New(handler).Info("ready", slog.Attr{}, slog.Group("empty"), "err", nil)
102 require.Equal(t, "<6>onechat: ready\n", out.String())
103 }
104
105 func TestDaemonLogGroupsDoNotChangeOtherLoggers(t *testing.T) {
106 var out strings.Builder
107
108 handler := &DaemonLogHandler{
109 out: &out,
110 journal: true,
111 mutex: &sync.Mutex{},
112 }
113
114 logger := slog.New(handler)
115 grouped := logger.With("before", 1).WithGroup("outer").With("saved", 2).WithGroup("inner")
116 grouped.Info("done", slog.Group("", "count", 3))
117 logger.Info("ready")
118 require.Equal(t, "<6>done before=1 outer.saved=2 outer.inner.count=3\n<6>ready\n", out.String())
119 }
120
121 func TestDaemonLogReturnsWriteFailure(t *testing.T) {
122 handler := &DaemonLogHandler{
123 out: DaemonLogBrokenWriter{},
124 mutex: &sync.Mutex{},
125 }
126
127 err := handler.Handle(context.Background(), slog.NewRecord(time.Now(), slog.LevelInfo, "ready", 0))
128 require.ErrorIs(t, err, io.ErrClosedPipe)
129 require.ErrorContains(t, err, "write daemon log")
130 }
131
132 type DaemonLogBrokenWriter struct{}
133
134 func (w DaemonLogBrokenWriter) Write(_ []byte) (int, error) {
135 return 0, io.ErrClosedPipe
136 }