github.com/ethereum/go-ethereum@v1.16.1/internal/testlog/testlog.go (about) 1 // Copyright 2019 The go-ethereum Authors 2 // This file is part of the go-ethereum library. 3 // 4 // The go-ethereum library is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU Lesser General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // The go-ethereum library is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU Lesser General Public License for more details. 13 // 14 // You should have received a copy of the GNU Lesser General Public License 15 // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>. 16 17 // Package testlog provides a log handler for unit tests. 18 package testlog 19 20 import ( 21 "bytes" 22 "context" 23 "fmt" 24 "log/slog" 25 "sync" 26 27 "github.com/ethereum/go-ethereum/log" 28 ) 29 30 const ( 31 termTimeFormat = "01-02|15:04:05.000" 32 ) 33 34 // T wraps methods from testing.T used by the test logger into an interface. 35 // It is specified so that unit tests can instantiate the logger with an 36 // implementation of T which can capture the output of logging statements 37 // from T.Logf, as this cannot be using testing.T. 38 type T interface { 39 Logf(format string, args ...any) 40 Helper() 41 } 42 43 // logger implements log.Logger such that all output goes to the unit test log via 44 // t.Logf(). All methods in between logger.Trace, logger.Debug, etc. are marked as test 45 // helpers, so the file and line number in unit test output correspond to the call site 46 // which emitted the log message. 47 type logger struct { 48 t T 49 l log.Logger 50 mu *sync.Mutex 51 h *bufHandler 52 } 53 54 type bufHandler struct { 55 buf []slog.Record 56 attrs []slog.Attr 57 level slog.Level 58 mu sync.Mutex 59 } 60 61 func (h *bufHandler) Handle(_ context.Context, r slog.Record) error { 62 h.mu.Lock() 63 defer h.mu.Unlock() 64 h.buf = append(h.buf, r) 65 return nil 66 } 67 68 func (h *bufHandler) Enabled(_ context.Context, lvl slog.Level) bool { 69 return lvl >= h.level 70 } 71 72 func (h *bufHandler) WithAttrs(attrs []slog.Attr) slog.Handler { 73 h.mu.Lock() 74 defer h.mu.Unlock() 75 records := make([]slog.Record, len(h.buf)) 76 copy(records[:], h.buf[:]) 77 return &bufHandler{ 78 buf: records, 79 attrs: append(h.attrs, attrs...), 80 level: h.level, 81 } 82 } 83 84 func (h *bufHandler) WithGroup(_ string) slog.Handler { 85 panic("not implemented") 86 } 87 88 // Logger returns a logger which logs to the unit test log of t. 89 func Logger(t T, level slog.Level) log.Logger { 90 handler := bufHandler{ 91 buf: []slog.Record{}, 92 attrs: []slog.Attr{}, 93 level: level, 94 } 95 return &logger{ 96 t: t, 97 l: log.NewLogger(&handler), 98 mu: new(sync.Mutex), 99 h: &handler, 100 } 101 } 102 103 func (l *logger) Handler() slog.Handler { 104 return l.l.Handler() 105 } 106 107 func (l *logger) Write(level slog.Level, msg string, ctx ...interface{}) {} 108 109 func (l *logger) Enabled(ctx context.Context, level slog.Level) bool { 110 return l.l.Enabled(ctx, level) 111 } 112 113 func (l *logger) Trace(msg string, ctx ...interface{}) { 114 l.t.Helper() 115 l.mu.Lock() 116 defer l.mu.Unlock() 117 l.l.Trace(msg, ctx...) 118 l.flush() 119 } 120 121 func (l *logger) Log(level slog.Level, msg string, ctx ...interface{}) { 122 l.t.Helper() 123 l.mu.Lock() 124 defer l.mu.Unlock() 125 l.l.Log(level, msg, ctx...) 126 l.flush() 127 } 128 129 func (l *logger) Debug(msg string, ctx ...interface{}) { 130 l.t.Helper() 131 l.mu.Lock() 132 defer l.mu.Unlock() 133 l.l.Debug(msg, ctx...) 134 l.flush() 135 } 136 137 func (l *logger) Info(msg string, ctx ...interface{}) { 138 l.t.Helper() 139 l.mu.Lock() 140 defer l.mu.Unlock() 141 l.l.Info(msg, ctx...) 142 l.flush() 143 } 144 145 func (l *logger) Warn(msg string, ctx ...interface{}) { 146 l.t.Helper() 147 l.mu.Lock() 148 defer l.mu.Unlock() 149 l.l.Warn(msg, ctx...) 150 l.flush() 151 } 152 153 func (l *logger) Error(msg string, ctx ...interface{}) { 154 l.t.Helper() 155 l.mu.Lock() 156 defer l.mu.Unlock() 157 l.l.Error(msg, ctx...) 158 l.flush() 159 } 160 161 func (l *logger) Crit(msg string, ctx ...interface{}) { 162 l.t.Helper() 163 l.mu.Lock() 164 defer l.mu.Unlock() 165 l.l.Crit(msg, ctx...) 166 l.flush() 167 } 168 169 func (l *logger) With(ctx ...interface{}) log.Logger { 170 newLogger := l.l.With(ctx...) 171 return &logger{l.t, newLogger, l.mu, newLogger.Handler().(*bufHandler)} 172 } 173 174 func (l *logger) New(ctx ...interface{}) log.Logger { 175 return l.With(ctx...) 176 } 177 178 // terminalFormat formats a message similarly to the NewTerminalHandler in the log package. 179 // The difference is that terminalFormat does not escape messages/attributes and does not pad attributes. 180 func (h *bufHandler) terminalFormat(r slog.Record) string { 181 buf := &bytes.Buffer{} 182 lvl := log.LevelAlignedString(r.Level) 183 attrs := []slog.Attr{} 184 r.Attrs(func(attr slog.Attr) bool { 185 attrs = append(attrs, attr) 186 return true 187 }) 188 189 attrs = append(h.attrs, attrs...) 190 191 fmt.Fprintf(buf, "%s[%s] %s ", lvl, r.Time.Format(termTimeFormat), r.Message) 192 if length := len(r.Message); length < 40 { 193 buf.Write(bytes.Repeat([]byte{' '}, 40-length)) 194 } 195 196 for _, attr := range attrs { 197 fmt.Fprintf(buf, " %s=%s", attr.Key, string(log.FormatSlogValue(attr.Value, nil))) 198 } 199 buf.WriteByte('\n') 200 return buf.String() 201 } 202 203 // flush writes all buffered messages and clears the buffer. 204 func (l *logger) flush() { 205 l.t.Helper() 206 l.h.mu.Lock() 207 defer l.h.mu.Unlock() 208 for _, r := range l.h.buf { 209 l.t.Logf("%s", l.h.terminalFormat(r)) 210 } 211 l.h.buf = nil 212 }