Source file src/log/slog/text_handler_test.go

     1  // Copyright 2022 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package slog
     6  
     7  import (
     8  	"bytes"
     9  	"context"
    10  	"errors"
    11  	"fmt"
    12  	"internal/testenv"
    13  	"io"
    14  	"strconv"
    15  	"strings"
    16  	"testing"
    17  	"time"
    18  )
    19  
    20  var testTime = time.Date(2000, 1, 2, 3, 4, 5, 0, time.UTC)
    21  
    22  var marshalerASCII = func() string {
    23  	b := make([]byte, 128)
    24  	for i := range b {
    25  		b[i] = byte(i)
    26  	}
    27  	return string(b)
    28  }()
    29  
    30  func TestTextHandler(t *testing.T) {
    31  	for _, test := range []struct {
    32  		name             string
    33  		attr             Attr
    34  		wantKey, wantVal string
    35  	}{
    36  		{
    37  			"unquoted",
    38  			Int("a", 1),
    39  			"a", "1",
    40  		},
    41  		{
    42  			"quoted",
    43  			String("x = y", `qu"o`),
    44  			`"x = y"`, `"qu\"o"`,
    45  		},
    46  		{
    47  			"String method",
    48  			Any("name", name{"Ren", "Hoek"}),
    49  			`name`, `"Hoek, Ren"`,
    50  		},
    51  		{
    52  			"struct",
    53  			Any("x", &struct{ A, b int }{A: 1, b: 2}),
    54  			`x`, `"&{A:1 b:2}"`,
    55  		},
    56  		{
    57  			"TextMarshaler",
    58  			Any("t", text{"abc"}),
    59  			`t`, `"text{\"abc\"}"`,
    60  		},
    61  		{
    62  			"TextMarshaler escapes",
    63  			Any("t", text{marshalerASCII}),
    64  			`t`, strconv.Quote(fmt.Sprintf("text{%q}", marshalerASCII)),
    65  		},
    66  		{
    67  			"TextMarshaler error",
    68  			Any("t", text{""}),
    69  			`t`, `"!ERROR:text: empty string"`,
    70  		},
    71  		{
    72  			"TextAppender",
    73  			Any("t", textAppend{"abc"}),
    74  			`t`, `"textAppend{\"abc\"}"`,
    75  		},
    76  		{
    77  			"TextAppender error",
    78  			Any("t", textAppend{""}),
    79  			`t`, `"!ERROR:textAppend: empty string"`,
    80  		},
    81  		{
    82  			"nil value",
    83  			Any("a", nil),
    84  			`a`, `<nil>`,
    85  		},
    86  	} {
    87  		t.Run(test.name, func(t *testing.T) {
    88  			for _, opts := range []struct {
    89  				name       string
    90  				opts       HandlerOptions
    91  				wantPrefix string
    92  				modKey     func(string) string
    93  			}{
    94  				{
    95  					"none",
    96  					HandlerOptions{},
    97  					`time=2000-01-02T03:04:05.000Z level=INFO msg="a message"`,
    98  					func(s string) string { return s },
    99  				},
   100  				{
   101  					"replace",
   102  					HandlerOptions{ReplaceAttr: upperCaseKey},
   103  					`TIME=2000-01-02T03:04:05.000Z LEVEL=INFO MSG="a message"`,
   104  					strings.ToUpper,
   105  				},
   106  			} {
   107  				t.Run(opts.name, func(t *testing.T) {
   108  					var buf bytes.Buffer
   109  					h := NewTextHandler(&buf, &opts.opts)
   110  					r := NewRecord(testTime, LevelInfo, "a message", 0)
   111  					r.AddAttrs(test.attr)
   112  					if err := h.Handle(context.Background(), r); err != nil {
   113  						t.Fatal(err)
   114  					}
   115  					got := buf.String()
   116  					// Remove final newline.
   117  					got = got[:len(got)-1]
   118  					want := opts.wantPrefix + " " + opts.modKey(test.wantKey) + "=" + test.wantVal
   119  					if got != want {
   120  						t.Errorf("\ngot  %s\nwant %s", got, want)
   121  					}
   122  				})
   123  			}
   124  		})
   125  	}
   126  }
   127  
   128  // for testing fmt.Sprint
   129  type name struct {
   130  	First, Last string
   131  }
   132  
   133  func (n name) String() string { return n.Last + ", " + n.First }
   134  
   135  // for testing TextMarshaler
   136  type text struct {
   137  	s string
   138  }
   139  
   140  func (t text) String() string { return t.s } // should be ignored
   141  
   142  func (t text) MarshalText() ([]byte, error) {
   143  	if t.s == "" {
   144  		return nil, errors.New("text: empty string")
   145  	}
   146  	return []byte(fmt.Sprintf("text{%q}", t.s)), nil
   147  }
   148  
   149  // for testing TextAppender
   150  type textAppend struct {
   151  	s string
   152  }
   153  
   154  func (t textAppend) String() string               { return t.s }
   155  func (t textAppend) MarshalText() ([]byte, error) { return []byte(t.s), nil }
   156  func (t textAppend) AppendText(b []byte) ([]byte, error) {
   157  	if t.s == "" {
   158  		return nil, errors.New("textAppend: empty string")
   159  	}
   160  	b = append(b, `textAppend{"`...)
   161  	b = append(b, t.s...)
   162  	b = append(b, `"}`...)
   163  	return b, nil
   164  }
   165  
   166  func TestTextHandlerPreformatted(t *testing.T) {
   167  	var buf bytes.Buffer
   168  	var h Handler = NewTextHandler(&buf, nil)
   169  	h = h.WithAttrs([]Attr{Duration("dur", time.Minute), Bool("b", true)})
   170  	// Also test omitting time.
   171  	r := NewRecord(time.Time{}, 0 /* 0 Level is INFO */, "m", 0)
   172  	r.AddAttrs(Int("a", 1))
   173  	if err := h.Handle(context.Background(), r); err != nil {
   174  		t.Fatal(err)
   175  	}
   176  	got := strings.TrimSuffix(buf.String(), "\n")
   177  	want := `level=INFO msg=m dur=1m0s b=true a=1`
   178  	if got != want {
   179  		t.Errorf("got %s, want %s", got, want)
   180  	}
   181  }
   182  
   183  func TestTextHandlerAlloc(t *testing.T) {
   184  	testenv.SkipIfOptimizationOff(t)
   185  	r := NewRecord(time.Now(), LevelInfo, "msg", 0)
   186  	for i := 0; i < 10; i++ {
   187  		r.AddAttrs(Int("x = y", i))
   188  	}
   189  	var h Handler = NewTextHandler(io.Discard, nil)
   190  	wantAllocs(t, 0, func() { h.Handle(context.Background(), r) })
   191  
   192  	h = h.WithGroup("s")
   193  	r.AddAttrs(Group("g", Int("a", 1)))
   194  	wantAllocs(t, 0, func() { h.Handle(context.Background(), r) })
   195  }
   196  
   197  func TestNeedsQuoting(t *testing.T) {
   198  	for _, test := range []struct {
   199  		in   string
   200  		want bool
   201  	}{
   202  		{"", true},
   203  		{"ab", false},
   204  		{"a=b", true},
   205  		{`"ab"`, true},
   206  		{"\a\b", true},
   207  		{"a\tb", true},
   208  		{"µåπ", false},
   209  		{"a b", true},
   210  		{"badutf8\xF6", true},
   211  	} {
   212  		got := needsQuoting(test.in)
   213  		if got != test.want {
   214  			t.Errorf("%q: got %t, want %t", test.in, got, test.want)
   215  		}
   216  	}
   217  }
   218  

View as plain text