golang.org/x/exp@v0.0.0-20240506185415-9bf2ced13842/slog/slogtest_test.go (about)

     1  // Copyright 2023 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_test
     6  
     7  import (
     8  	"bytes"
     9  	"encoding/json"
    10  	"fmt"
    11  	"io"
    12  	"strings"
    13  	"testing"
    14  
    15  	"golang.org/x/exp/slog"
    16  	"golang.org/x/exp/slog/slogtest"
    17  )
    18  
    19  func TestSlogtest(t *testing.T) {
    20  	for _, test := range []struct {
    21  		name  string
    22  		new   func(io.Writer) slog.Handler
    23  		parse func([]byte) (map[string]any, error)
    24  	}{
    25  		{"JSON", func(w io.Writer) slog.Handler { return slog.NewJSONHandler(w, nil) }, parseJSON},
    26  		{"Text", func(w io.Writer) slog.Handler { return slog.NewTextHandler(w, nil) }, parseText},
    27  	} {
    28  		t.Run(test.name, func(t *testing.T) {
    29  			var buf bytes.Buffer
    30  			h := test.new(&buf)
    31  			results := func() []map[string]any {
    32  				ms, err := parseLines(buf.Bytes(), test.parse)
    33  				if err != nil {
    34  					t.Fatal(err)
    35  				}
    36  				return ms
    37  			}
    38  			if err := slogtest.TestHandler(h, results); err != nil {
    39  				t.Fatal(err)
    40  			}
    41  		})
    42  	}
    43  }
    44  
    45  func parseLines(src []byte, parse func([]byte) (map[string]any, error)) ([]map[string]any, error) {
    46  	var records []map[string]any
    47  	for _, line := range bytes.Split(src, []byte{'\n'}) {
    48  		if len(line) == 0 {
    49  			continue
    50  		}
    51  		m, err := parse(line)
    52  		if err != nil {
    53  			return nil, fmt.Errorf("%s: %w", string(line), err)
    54  		}
    55  		records = append(records, m)
    56  	}
    57  	return records, nil
    58  }
    59  
    60  func parseJSON(bs []byte) (map[string]any, error) {
    61  	var m map[string]any
    62  	if err := json.Unmarshal(bs, &m); err != nil {
    63  		return nil, err
    64  	}
    65  	return m, nil
    66  }
    67  
    68  // parseText parses the output of a single call to TextHandler.Handle.
    69  // It can parse the output of the tests in this package,
    70  // but it doesn't handle quoted keys or values.
    71  // It doesn't need to handle all cases, because slogtest deliberately
    72  // uses simple inputs so handler writers can focus on testing
    73  // handler behavior, not parsing.
    74  func parseText(bs []byte) (map[string]any, error) {
    75  	top := map[string]any{}
    76  	s := string(bytes.TrimSpace(bs))
    77  	for len(s) > 0 {
    78  		kv, rest, _ := strings.Cut(s, " ") // assumes exactly one space between attrs
    79  		k, value, found := strings.Cut(kv, "=")
    80  		if !found {
    81  			return nil, fmt.Errorf("no '=' in %q", kv)
    82  		}
    83  		keys := strings.Split(k, ".")
    84  		// Populate a tree of maps for a dotted path such as "a.b.c=x".
    85  		m := top
    86  		for _, key := range keys[:len(keys)-1] {
    87  			x, ok := m[key]
    88  			var m2 map[string]any
    89  			if !ok {
    90  				m2 = map[string]any{}
    91  				m[key] = m2
    92  			} else {
    93  				m2, ok = x.(map[string]any)
    94  				if !ok {
    95  					return nil, fmt.Errorf("value for %q in composite key %q is not map[string]any", key, k)
    96  
    97  				}
    98  			}
    99  			m = m2
   100  		}
   101  		m[keys[len(keys)-1]] = value
   102  		s = rest
   103  	}
   104  	return top, nil
   105  }