Skip to content

Commit f64aa6e

Browse files
authored
internal/testlog: fix log output from sub-loggers (#31539)
When we instantiate a sub-logger via `go-ethereum/internal/testlog/logger.With`, we copy the reference to the `bufHandler` from the parent logger. However, internally, `go-ethereum/internal/testlog/logger.With` calls `log/slog/Logger.With` which creates a new handler instance (via `internal/bufHandler.WithAttrs`). This PR modifies sub-logger instantiation to use the newly-instantiated handler, instead of copying the reference from the parent instance. The type cast from `slog.Handler` to `*bufHandler` in `internal/testlog/Logger.With` is safe here because a `internal/testlog/Logger` can only be instantiated with a `*bufHandler` as the underlying handler type. Note, that I've also removed a pre-existing method that broke the above assumption. However, this method is not used in our codebase. I'm not sure if the assumption holds for forks of geth (e.g. optimism has modified the testlogger somewhat allowing test loggers to accept arbitrary handler types), but it seems okay to break API compatibility given that this is in the `internal` package. closes #31533
1 parent 9b4eab6 commit f64aa6e

File tree

3 files changed

+81
-16
lines changed

3 files changed

+81
-16
lines changed

internal/testlog/testlog.go

Lines changed: 13 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ import (
2323
"fmt"
2424
"log/slog"
2525
"sync"
26-
"testing"
2726

2827
"github.com/ethereum/go-ethereum/log"
2928
)
@@ -32,12 +31,21 @@ const (
3231
termTimeFormat = "01-02|15:04:05.000"
3332
)
3433

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+
3543
// logger implements log.Logger such that all output goes to the unit test log via
3644
// t.Logf(). All methods in between logger.Trace, logger.Debug, etc. are marked as test
3745
// helpers, so the file and line number in unit test output correspond to the call site
3846
// which emitted the log message.
3947
type logger struct {
40-
t *testing.T
48+
t T
4149
l log.Logger
4250
mu *sync.Mutex
4351
h *bufHandler
@@ -78,7 +86,7 @@ func (h *bufHandler) WithGroup(_ string) slog.Handler {
7886
}
7987

8088
// Logger returns a logger which logs to the unit test log of t.
81-
func Logger(t *testing.T, level slog.Level) log.Logger {
89+
func Logger(t T, level slog.Level) log.Logger {
8290
handler := bufHandler{
8391
buf: []slog.Record{},
8492
attrs: []slog.Attr{},
@@ -92,17 +100,6 @@ func Logger(t *testing.T, level slog.Level) log.Logger {
92100
}
93101
}
94102

95-
// LoggerWithHandler returns
96-
func LoggerWithHandler(t *testing.T, handler slog.Handler) log.Logger {
97-
var bh bufHandler
98-
return &logger{
99-
t: t,
100-
l: log.NewLogger(handler),
101-
mu: new(sync.Mutex),
102-
h: &bh,
103-
}
104-
}
105-
106103
func (l *logger) Handler() slog.Handler {
107104
return l.l.Handler()
108105
}
@@ -170,7 +167,8 @@ func (l *logger) Crit(msg string, ctx ...interface{}) {
170167
}
171168

172169
func (l *logger) With(ctx ...interface{}) log.Logger {
173-
return &logger{l.t, l.l.With(ctx...), l.mu, l.h}
170+
newLogger := l.l.With(ctx...)
171+
return &logger{l.t, newLogger, l.mu, newLogger.Handler().(*bufHandler)}
174172
}
175173

176174
func (l *logger) New(ctx ...interface{}) log.Logger {

internal/testlog/testlog_test.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package testlog
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
"io"
7+
"strings"
8+
"testing"
9+
10+
"github.com/ethereum/go-ethereum/log"
11+
)
12+
13+
type mockT struct {
14+
out io.Writer
15+
}
16+
17+
func (t *mockT) Helper() {
18+
// noop for the purposes of unit tests
19+
}
20+
21+
func (t *mockT) Logf(format string, args ...any) {
22+
// we could gate this operation in a mutex, but because testlogger
23+
// only calls Logf with its internal mutex held, we just write output here
24+
var lineBuf bytes.Buffer
25+
if _, err := fmt.Fprintf(&lineBuf, format, args...); err != nil {
26+
panic(err)
27+
}
28+
// The timestamp is locale-dependent, so we want to trim that off
29+
// "INFO [01-01|00:00:00.000] a message ..." -> "a message..."
30+
sanitized := strings.Split(lineBuf.String(), "]")[1]
31+
if _, err := t.out.Write([]byte(sanitized)); err != nil {
32+
panic(err)
33+
}
34+
}
35+
36+
func TestLogging(t *testing.T) {
37+
tests := []struct {
38+
name string
39+
expected string
40+
run func(t *mockT)
41+
}{
42+
{
43+
"SubLogger",
44+
` Visible
45+
Hide and seek foobar=123
46+
Also visible
47+
`,
48+
func(t *mockT) {
49+
l := Logger(t, log.LevelInfo)
50+
subLogger := l.New("foobar", 123)
51+
52+
l.Info("Visible")
53+
subLogger.Info("Hide and seek")
54+
l.Info("Also visible")
55+
},
56+
},
57+
}
58+
59+
for _, tc := range tests {
60+
outp := bytes.Buffer{}
61+
mock := mockT{&outp}
62+
tc.run(&mock)
63+
if outp.String() != tc.expected {
64+
fmt.Printf("output mismatch.\nwant: '%s'\ngot: '%s'\n", tc.expected, outp.String())
65+
}
66+
}
67+
}

tests/testdata

0 commit comments

Comments
 (0)