Take the console location from the record (closes #36)
ConsoleHandler found the file and line it prints by walking a fixed number of stack frames up from itself, which only fit a record logged through a slog.Logger and delivered through MultiplexHandler. It now reads them from the record's PC, the call site slog stores, so a ConsoleHandler used with slog.New and a record passed to Handle directly print the right location too. A record whose PC is zero prints ???:0 as before. The README drops its warning about the wrong location and says how to set PC instead. Model: opus-5-5
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
package simplelog
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// These tests sit inside the package so they can read the console line
|
||||
// from a buffer instead of from stdout.
|
||||
|
||||
// lineOf calls logCall and returns "file:line" for the line lineOf was
|
||||
// called from. Each test writes its log call inside logCall on that same
|
||||
// line, so the result is the location the console line must name.
|
||||
func lineOf(logCall func()) string {
|
||||
_, file, line, _ := runtime.Caller(1)
|
||||
|
||||
logCall()
|
||||
|
||||
return fmt.Sprintf("%s:%d", file, line)
|
||||
}
|
||||
|
||||
// wantLocation fails the test unless the console line names location as
|
||||
// where the record "casting" was logged.
|
||||
func wantLocation(t *testing.T, output, location string) {
|
||||
t.Helper()
|
||||
|
||||
if !strings.Contains(output, location+": casting") {
|
||||
t.Fatalf("console line %q does not name %s", output, location)
|
||||
}
|
||||
}
|
||||
|
||||
// The default handler as simplelog installs it when stdout is a terminal:
|
||||
// a MultiplexHandler holding a ConsoleHandler.
|
||||
func TestConsoleHandlerNamesCallSiteThroughDefaultHandler(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var output bytes.Buffer
|
||||
|
||||
logger := slog.New(&MultiplexHandler{handlers: []ExtendedHandler{
|
||||
&ConsoleHandler{out: &output},
|
||||
}})
|
||||
|
||||
want := lineOf(func() { logger.Info("casting") })
|
||||
|
||||
wantLocation(t, output.String(), want)
|
||||
}
|
||||
|
||||
func TestConsoleHandlerNamesCallSiteUsedWithSlogNew(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var output bytes.Buffer
|
||||
|
||||
logger := slog.New(&ConsoleHandler{out: &output})
|
||||
|
||||
want := lineOf(func() { logger.Info("casting") })
|
||||
|
||||
wantLocation(t, output.String(), want)
|
||||
}
|
||||
|
||||
// The record is built as the log/slog package documentation shows for a
|
||||
// function that logs on its caller's behalf: runtime.Callers supplies the
|
||||
// PC.
|
||||
func TestConsoleHandlerNamesCallSiteOfRecordPassedToHandle(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var (
|
||||
output bytes.Buffer
|
||||
pcs [1]uintptr
|
||||
)
|
||||
|
||||
want := lineOf(func() { runtime.Callers(1, pcs[:]) })
|
||||
|
||||
record := slog.NewRecord(time.Now(), slog.LevelInfo, "casting", pcs[0])
|
||||
|
||||
err := (&ConsoleHandler{out: &output}).Handle(context.Background(), record)
|
||||
if err != nil {
|
||||
t.Fatalf("Handle: %v", err)
|
||||
}
|
||||
|
||||
wantLocation(t, output.String(), want)
|
||||
}
|
||||
|
||||
func TestConsoleHandlerPrintsPlaceholderWithoutPC(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var output bytes.Buffer
|
||||
|
||||
record := slog.NewRecord(time.Now(), slog.LevelInfo, "casting", 0)
|
||||
|
||||
err := (&ConsoleHandler{out: &output}).Handle(context.Background(), record)
|
||||
if err != nil {
|
||||
t.Fatalf("Handle: %v", err)
|
||||
}
|
||||
|
||||
wantLocation(t, output.String(), "???:0")
|
||||
}
|
||||
Reference in New Issue
Block a user