| // Copyright 2022 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package slog |
| |
| import ( |
| "bytes" |
| "context" |
| "errors" |
| "fmt" |
| "io" |
| "regexp" |
| "strings" |
| "testing" |
| "time" |
| ) |
| |
| var testTime = time.Date(2000, 1, 2, 3, 4, 5, 0, time.UTC) |
| |
| func TestTextHandler(t *testing.T) { |
| for _, test := range []struct { |
| name string |
| attr Attr |
| wantKey, wantVal string |
| }{ |
| { |
| "unquoted", |
| Int("a", 1), |
| "a", "1", |
| }, |
| { |
| "quoted", |
| String("x = y", `qu"o`), |
| `"x = y"`, `"qu\"o"`, |
| }, |
| { |
| "String method", |
| Any("name", name{"Ren", "Hoek"}), |
| `name`, `"Hoek, Ren"`, |
| }, |
| { |
| "struct", |
| Any("x", &struct{ A, b int }{A: 1, b: 2}), |
| `x`, `"&{A:1 b:2}"`, |
| }, |
| { |
| "TextMarshaler", |
| Any("t", text{"abc"}), |
| `t`, `"text{\"abc\"}"`, |
| }, |
| { |
| "TextMarshaler error", |
| Any("t", text{""}), |
| `t`, `"!ERROR:text: empty string"`, |
| }, |
| { |
| "nil value", |
| Any("a", nil), |
| `a`, `<nil>`, |
| }, |
| } { |
| t.Run(test.name, func(t *testing.T) { |
| for _, opts := range []struct { |
| name string |
| opts HandlerOptions |
| wantPrefix string |
| modKey func(string) string |
| }{ |
| { |
| "none", |
| HandlerOptions{}, |
| `time=2000-01-02T03:04:05.000Z level=INFO msg="a message"`, |
| func(s string) string { return s }, |
| }, |
| { |
| "replace", |
| HandlerOptions{ReplaceAttr: upperCaseKey}, |
| `TIME=2000-01-02T03:04:05.000Z LEVEL=INFO MSG="a message"`, |
| strings.ToUpper, |
| }, |
| } { |
| t.Run(opts.name, func(t *testing.T) { |
| var buf bytes.Buffer |
| h := opts.opts.NewTextHandler(&buf) |
| r := NewRecord(testTime, LevelInfo, "a message", 0) |
| r.AddAttrs(test.attr) |
| if err := h.Handle(context.Background(), r); err != nil { |
| t.Fatal(err) |
| } |
| got := buf.String() |
| // Remove final newline. |
| got = got[:len(got)-1] |
| want := opts.wantPrefix + " " + opts.modKey(test.wantKey) + "=" + test.wantVal |
| if got != want { |
| t.Errorf("\ngot %s\nwant %s", got, want) |
| } |
| }) |
| } |
| }) |
| } |
| } |
| |
| // for testing fmt.Sprint |
| type name struct { |
| First, Last string |
| } |
| |
| func (n name) String() string { return n.Last + ", " + n.First } |
| |
| // for testing TextMarshaler |
| type text struct { |
| s string |
| } |
| |
| func (t text) String() string { return t.s } // should be ignored |
| |
| func (t text) MarshalText() ([]byte, error) { |
| if t.s == "" { |
| return nil, errors.New("text: empty string") |
| } |
| return []byte(fmt.Sprintf("text{%q}", t.s)), nil |
| } |
| |
| func TestTextHandlerSource(t *testing.T) { |
| var buf bytes.Buffer |
| h := HandlerOptions{AddSource: true}.NewTextHandler(&buf) |
| r := NewRecord(testTime, LevelInfo, "m", callerPC(2)) |
| if err := h.Handle(context.Background(), r); err != nil { |
| t.Fatal(err) |
| } |
| if got := buf.String(); !sourceRegexp.MatchString(got) { |
| t.Errorf("got\n%q\nwanted to match %s", got, sourceRegexp) |
| } |
| } |
| |
| var sourceRegexp = regexp.MustCompile(`source="?([A-Z]:)?[^:]+text_handler_test\.go:\d+"? msg`) |
| |
| func TestSourceRegexp(t *testing.T) { |
| for _, s := range []string{ |
| `source=/tmp/path/to/text_handler_test.go:23 msg=m`, |
| `source=C:\windows\path\text_handler_test.go:23 msg=m"`, |
| `source="/tmp/tmp.XcGZ9cG9Xb/with spaces/exp/slog/text_handler_test.go:95" msg=m`, |
| } { |
| if !sourceRegexp.MatchString(s) { |
| t.Errorf("failed to match %s", s) |
| } |
| } |
| } |
| |
| func TestTextHandlerPreformatted(t *testing.T) { |
| var buf bytes.Buffer |
| var h Handler = NewTextHandler(&buf) |
| h = h.WithAttrs([]Attr{Duration("dur", time.Minute), Bool("b", true)}) |
| // Also test omitting time. |
| r := NewRecord(time.Time{}, 0 /* 0 Level is INFO */, "m", 0) |
| r.AddAttrs(Int("a", 1)) |
| if err := h.Handle(context.Background(), r); err != nil { |
| t.Fatal(err) |
| } |
| got := strings.TrimSuffix(buf.String(), "\n") |
| want := `level=INFO msg=m dur=1m0s b=true a=1` |
| if got != want { |
| t.Errorf("got %s, want %s", got, want) |
| } |
| } |
| |
| func TestTextHandlerAlloc(t *testing.T) { |
| r := NewRecord(time.Now(), LevelInfo, "msg", 0) |
| for i := 0; i < 10; i++ { |
| r.AddAttrs(Int("x = y", i)) |
| } |
| var h Handler = NewTextHandler(io.Discard) |
| wantAllocs(t, 0, func() { h.Handle(context.Background(), r) }) |
| |
| h = h.WithGroup("s") |
| r.AddAttrs(Group("g", Int("a", 1))) |
| wantAllocs(t, 0, func() { h.Handle(context.Background(), r) }) |
| } |
| |
| func TestNeedsQuoting(t *testing.T) { |
| for _, test := range []struct { |
| in string |
| want bool |
| }{ |
| {"", true}, |
| {"ab", false}, |
| {"a=b", true}, |
| {`"ab"`, true}, |
| {"\a\b", true}, |
| {"a\tb", true}, |
| {"µåπ", false}, |
| } { |
| got := needsQuoting(test.in) |
| if got != test.want { |
| t.Errorf("%q: got %t, want %t", test.in, got, test.want) |
| } |
| } |
| } |