From a86f3ddf04fa0c886a202a8d02f349c0ff68c782 Mon Sep 17 00:00:00 2001 From: david-littlefarmer Date: Wed, 30 Aug 2023 18:08:04 +0200 Subject: [PATCH] total rewrite --- attributes.go | 2 +- attributes_test.go | 21 +- color.go | 74 ++++--- color_test.go | 49 +++-- devslog.go | 472 +++++++++++++++++++++++++++------------ devslog_test.go | 535 ++++++++++++++++++++++++++++++--------------- 6 files changed, 774 insertions(+), 379 deletions(-) diff --git a/attributes.go b/attributes.go index 41faf39..a876ff5 100644 --- a/attributes.go +++ b/attributes.go @@ -19,7 +19,7 @@ func (a attributes) Less(i, j int) bool { func (a attributes) padding(c foregroundColor) int { var padding int for _, e := range a { - color := len(cs(e.Key, c)) + color := len(cs([]byte(e.Key), c)) if color > padding { padding = color } diff --git a/attributes_test.go b/attributes_test.go index e4483d2..02e3d28 100644 --- a/attributes_test.go +++ b/attributes_test.go @@ -5,7 +5,16 @@ import ( "testing" ) -func Test_AttributesLen(t *testing.T) { +func Test_Attributes(t *testing.T) { + test_AttributesLen(t) + test_AttributesSwap(t) + test_AttributesLess(t) + test_AttributesLessGroupTrue(t) + test_AttributesLessGroupFalse(t) + test_AttributesPadding(t) +} + +func test_AttributesLen(t *testing.T) { someValue := slog.StringValue("value") attrs := attributes{ slog.Attr{Key: "key1", Value: someValue}, @@ -21,7 +30,7 @@ func Test_AttributesLen(t *testing.T) { } } -func Test_AttributesSwap(t *testing.T) { +func test_AttributesSwap(t *testing.T) { attr1 := slog.Attr{Key: "key1", Value: slog.StringValue("value")} attr2 := slog.Attr{Key: "key2", Value: slog.StringValue("value")} attrs := attributes{ @@ -36,7 +45,7 @@ func Test_AttributesSwap(t *testing.T) { } } -func Test_AttributesLess(t *testing.T) { +func test_AttributesLess(t *testing.T) { someValue := slog.StringValue("value") attrs := attributes{ slog.Attr{Key: "key1", Value: someValue}, @@ -50,7 +59,7 @@ func Test_AttributesLess(t *testing.T) { } } -func Test_AttributesLessGroupTrue(t *testing.T) { +func test_AttributesLessGroupTrue(t *testing.T) { attrs := attributes{ slog.String("key1", "someValue"), slog.Group("key2", slog.String("someString", "someValue")), @@ -63,7 +72,7 @@ func Test_AttributesLessGroupTrue(t *testing.T) { } } -func Test_AttributesLessGroupFalse(t *testing.T) { +func test_AttributesLessGroupFalse(t *testing.T) { attrs := attributes{ slog.Group("key1", slog.String("someString", "someValue")), slog.String("key2", "someValue"), @@ -76,7 +85,7 @@ func Test_AttributesLessGroupFalse(t *testing.T) { } } -func Test_AttributesPadding(t *testing.T) { +func test_AttributesPadding(t *testing.T) { someValue := slog.StringValue("value") attrs := attributes{ slog.Attr{Key: "key1", Value: someValue}, diff --git a/color.go b/color.go index 21fce16..206ab0f 100644 --- a/color.go +++ b/color.go @@ -1,56 +1,64 @@ package devslog -import "fmt" - type ( - foregroundColor string - backgroundColor string - commonValuesColor string + foregroundColor []byte + backgroundColor []byte + commonValuesColor []byte ) -const ( +var ( // Foreground colors - fgBlack foregroundColor = "\x1b[30m" - fgRed foregroundColor = "\x1b[31m" - fgGreen foregroundColor = "\x1b[32m" - fgYellow foregroundColor = "\x1b[33m" - fgBlue foregroundColor = "\x1b[34m" - fgMagenta foregroundColor = "\x1b[35m" - fgCyan foregroundColor = "\x1b[36m" - fgWhite foregroundColor = "\x1b[37m" + fgBlack foregroundColor = []byte("\x1b[30m") + fgRed foregroundColor = []byte("\x1b[31m") + fgGreen foregroundColor = []byte("\x1b[32m") + fgYellow foregroundColor = []byte("\x1b[33m") + fgBlue foregroundColor = []byte("\x1b[34m") + fgMagenta foregroundColor = []byte("\x1b[35m") + fgCyan foregroundColor = []byte("\x1b[36m") + fgWhite foregroundColor = []byte("\x1b[37m") // Background colors - bgBlack backgroundColor = "\x1b[40m" - bgRed backgroundColor = "\x1b[41m" - bgGreen backgroundColor = "\x1b[42m" - bgYellow backgroundColor = "\x1b[43m" - bgBlue backgroundColor = "\x1b[44m" - bgMagenta backgroundColor = "\x1b[45m" - bgCyan backgroundColor = "\x1b[46m" - bgWhite backgroundColor = "\x1b[47m" + bgBlack backgroundColor = []byte("\x1b[40m") + bgRed backgroundColor = []byte("\x1b[41m") + bgGreen backgroundColor = []byte("\x1b[42m") + bgYellow backgroundColor = []byte("\x1b[43m") + bgBlue backgroundColor = []byte("\x1b[44m") + bgMagenta backgroundColor = []byte("\x1b[45m") + bgCyan backgroundColor = []byte("\x1b[46m") + bgWhite backgroundColor = []byte("\x1b[47m") // Common consts - resetColor commonValuesColor = "\x1b[0m" - faintColor commonValuesColor = "\x1b[2m" - underlineColor commonValuesColor = "\x1b[4m" + resetColor commonValuesColor = []byte("\x1b[0m") + faintColor commonValuesColor = []byte("\x1b[2m") + underlineColor commonValuesColor = []byte("\x1b[4m") ) // Color string foreground -func cs(text string, fgColor foregroundColor) string { - return fmt.Sprintf("%v%v%v", fgColor, text, resetColor) +func cs(b []byte, fgColor foregroundColor) []byte { + b = append(fgColor, b...) + b = append(b, resetColor...) + return b } // Color string fainted -func csf(text string, fgColor foregroundColor) string { - return fmt.Sprintf("%v%v%v%v", fgColor, faintColor, text, resetColor) +func csf(b []byte, fgColor foregroundColor) []byte { + b = append(fgColor, b...) + b = append(faintColor, b...) + b = append(b, resetColor...) + return b } // Color string background -func csb(text string, fgColor foregroundColor, bgColor backgroundColor) string { - return fmt.Sprintf("%v%v%v%v", fgColor, bgColor, text, resetColor) +func csb(b []byte, fgColor foregroundColor, bgColor backgroundColor) []byte { + b = append(fgColor, b...) + b = append(bgColor, b...) + b = append(b, resetColor...) + return b } // Underline text -func ul(text string) string { - return fmt.Sprintf("%v%v%v", underlineColor, text, resetColor) +func ul(b []byte) []byte { + b = append(underlineColor, b...) + b = append(b, resetColor...) + return b } diff --git a/color_test.go b/color_test.go index f908a8b..158ddb8 100644 --- a/color_test.go +++ b/color_test.go @@ -1,41 +1,50 @@ package devslog import ( + "bytes" "testing" ) -func Test_ColorCs(t *testing.T) { - expected := "\x1b[32mHello\x1b[0m" - result := cs("Hello", fgGreen) +func Test_Color(t *testing.T) { + b := []byte("Hello") + test_ColorCs(t, b) + test_ColorCsf(t, b) + test_ColorCsb(t, b) + test_ColorUl(t, b) +} + +func test_ColorCs(t *testing.T, b []byte) { + result := cs(b, fgGreen) - if result != expected { - t.Errorf("Expected: %q, but got: %q", expected, result) + expected := []byte("\x1b[32mHello\x1b[0m") + if !bytes.Equal(expected, result) { + t.Errorf("\nExpected: %s\nResult: %s\nExpected: %[1]q\nResult: %[2]q", expected, result) } } -func Test_ColorCsf(t *testing.T) { - expected := "\x1b[34m\x1b[2mHello\x1b[0m" - result := csf("Hello", fgBlue) +func test_ColorCsf(t *testing.T, b []byte) { + result := csf(b, fgBlue) - if result != expected { - t.Errorf("Expected: %q, but got: %q", expected, result) + expected := []byte("\x1b[2m\x1b[34mHello\x1b[0m") + if !bytes.Equal(expected, result) { + t.Errorf("\nExpected: %s\nResult: %s\nExpected: %[1]q\nResult: %[2]q", expected, result) } } -func Test_ColorCsb(t *testing.T) { - expected := "\x1b[35m\x1b[43mHello\x1b[0m" - result := csb("Hello", fgMagenta, bgYellow) +func test_ColorCsb(t *testing.T, b []byte) { + result := csb(b, fgYellow, bgRed) - if result != expected { - t.Errorf("Expected: %q, but got: %q", expected, result) + expected := []byte("\x1b[41m\x1b[33mHello\x1b[0m") + if !bytes.Equal(expected, result) { + t.Errorf("\nExpected: %s\nResult: %s\nExpected: %[1]q\nResult: %[2]q", expected, result) } } -func Test_ColorUl(t *testing.T) { - expected := "\x1b[4mHello\x1b[0m" - result := ul("Hello") +func test_ColorUl(t *testing.T, b []byte) { + result := ul(b) - if result != expected { - t.Errorf("Expected: %q, but got: %q", expected, result) + expected := []byte("\x1b[4mHello\x1b[0m") + if !bytes.Equal(expected, result) { + t.Errorf("\nExpected: %s\nResult: %s\nExpected: %[1]q\nResult: %[2]q", expected, result) } } diff --git a/devslog.go b/devslog.go index b5e64cb..bac82a9 100644 --- a/devslog.go +++ b/devslog.go @@ -1,17 +1,19 @@ package devslog import ( + "bytes" "context" - "encoding/json" "fmt" "io" "log/slog" "net/url" + "reflect" "runtime" "sort" "strconv" "strings" "sync" + "time" ) type developHandler struct { @@ -40,17 +42,17 @@ type groupOrAttrs struct { attrs []slog.Attr } -func NewHandler(out io.Writer, opts *Options) *developHandler { +func NewHandler(out io.Writer, o *Options) *developHandler { h := &developHandler{out: out, mu: &sync.Mutex{}} - if opts != nil { - h.opts = *opts + if o != nil { + h.opts = *o - if opts.HandlerOptions != nil { - h.opts.HandlerOptions = opts.HandlerOptions - if opts.Level == nil { + if o.HandlerOptions != nil { + h.opts.HandlerOptions = o.HandlerOptions + if o.Level == nil { h.opts.Level = slog.LevelInfo } else { - h.opts.HandlerOptions.Level = opts.HandlerOptions.Level + h.opts.HandlerOptions.Level = o.HandlerOptions.Level } } else { h.opts.HandlerOptions = &slog.HandlerOptions{ @@ -58,11 +60,11 @@ func NewHandler(out io.Writer, opts *Options) *developHandler { } } - if opts.MaxSlicePrintSize == 0 { + if o.MaxSlicePrintSize == 0 { h.opts.MaxSlicePrintSize = 50 } - if opts.TimeFormat == "" { + if o.TimeFormat == "" { h.opts.TimeFormat = "[15:06:05]" } } else { @@ -76,24 +78,24 @@ func NewHandler(out io.Writer, opts *Options) *developHandler { return h } -func (h *developHandler) Enabled(ctx context.Context, level slog.Level) bool { - return level >= h.opts.Level.Level() +func (h *developHandler) Enabled(ctx context.Context, l slog.Level) bool { + return l >= h.opts.Level.Level() } -func (h *developHandler) WithGroup(name string) slog.Handler { - if name == "" { +func (h *developHandler) WithGroup(s string) slog.Handler { + if s == "" { return h } - return h.withGroupOrAttrs(groupOrAttrs{group: name}) + return h.withGroupOrAttrs(groupOrAttrs{group: s}) } -func (h *developHandler) WithAttrs(attrs []slog.Attr) slog.Handler { - if len(attrs) == 0 { +func (h *developHandler) WithAttrs(as []slog.Attr) slog.Handler { + if len(as) == 0 { return h } - return h.withGroupOrAttrs(groupOrAttrs{attrs: attrs}) + return h.withGroupOrAttrs(groupOrAttrs{attrs: as}) } func (h *developHandler) withGroupOrAttrs(goa groupOrAttrs) *developHandler { @@ -105,71 +107,74 @@ func (h *developHandler) withGroupOrAttrs(goa groupOrAttrs) *developHandler { } func (h *developHandler) Handle(ctx context.Context, r slog.Record) error { - buf := make([]byte, 0, 1024) - buf = fmt.Appendf(buf, "%s ", csf(r.Time.Format(h.opts.TimeFormat), fgWhite)) - buf = h.formatSourceInfo(buf, &r) - buf = h.levelMessage(buf, &r) - buf = h.processAttributes(buf, &r) + b := make([]byte, 0, 1024) + b = append(b, csf([]byte(r.Time.Format(h.opts.TimeFormat)), fgWhite)...) + b = append(b, ' ') + b = h.formatSourceInfo(b, &r) + b = h.levelMessage(b, &r) + b = h.processAttributes(b, &r) h.mu.Lock() defer h.mu.Unlock() - _, err := h.out.Write(buf) + _, err := h.out.Write(b) return err } -func (h *developHandler) formatSourceInfo(buf []byte, r *slog.Record) []byte { +func (h *developHandler) formatSourceInfo(b []byte, r *slog.Record) []byte { if h.opts.AddSource { - at := cs("@@@", fgBlue) - frame, _ := runtime.CallersFrames([]uintptr{r.PC}).Next() - source := ul(cs(frame.File, fgYellow)) - line := cs(strconv.Itoa(frame.Line), fgRed) - buf = fmt.Appendf(buf, "%s %s:%s\n", at, source, line) + f, _ := runtime.CallersFrames([]uintptr{r.PC}).Next() + b = append(b, cs([]byte("@@@"), fgBlue)...) + b = append(b, ' ') + b = append(b, ul(cs([]byte(f.File), fgYellow))...) + b = append(b, ':') + b = append(b, cs([]byte(strconv.Itoa(f.Line)), fgRed)...) + b = append(b, '\n') } - return buf + return b } -func (h *developHandler) levelMessage(buf []byte, r *slog.Record) []byte { +func (h *developHandler) levelMessage(b []byte, r *slog.Record) []byte { var bgColor backgroundColor var fgColor foregroundColor - var lvlStr string + var ls string if h.opts.ReplaceAttr != nil { a := h.opts.ReplaceAttr(nil, slog.Any(slog.LevelKey, r.Level)) - lvlStr = a.Value.String() + ls = a.Value.String() if a.Key != "level" { r.AddAttrs(a) } } else { - lvlStr = r.Level.String() + ls = r.Level.String() } - level := r.Level + lr := r.Level switch { - case level < 0: + case lr < 0: bgColor, fgColor = bgBlue, fgBlue - case level < 4: + case lr < 4: bgColor, fgColor = bgGreen, fgGreen - case level < 8: + case lr < 8: bgColor, fgColor = bgYellow, fgYellow default: bgColor, fgColor = bgRed, fgRed } - lvl := csb(" "+lvlStr+" ", fgBlack, bgColor) - msg := cs(r.Message, fgColor) + b = append(b, csb([]byte(" "+ls+" "), fgBlack, bgColor)...) + b = append(b, ' ') + b = append(b, cs([]byte(r.Message), fgColor)...) + b = append(b, '\n') - buf = fmt.Appendf(buf, "%s %s\n", lvl, msg) - - return buf + return b } -func (h *developHandler) processAttributes(buf []byte, r *slog.Record) []byte { - var attrs attributes +func (h *developHandler) processAttributes(b []byte, r *slog.Record) []byte { + var as attributes if r.NumAttrs() != 0 { r.Attrs(func(a slog.Attr) bool { - attrs = append(attrs, a) + as = append(as, a) return true }) } @@ -183,175 +188,352 @@ func (h *developHandler) processAttributes(buf []byte, r *slog.Record) []byte { for i := len(goas) - 1; i >= 0; i-- { if goas[i].group != "" { - newGroup := slog.Attr{ + ng := slog.Attr{ Key: goas[i].group, - Value: slog.GroupValue(attrs...), + Value: slog.GroupValue(as...), } - attrs = attributes{newGroup} + as = attributes{ng} } else { - attrs = append(attrs, goas[i].attrs...) + as = append(as, goas[i].attrs...) } } - buf = h.colorize(buf, attrs, 0, []string{}) - buf = append(buf, '\n') - return buf + b = h.colorize(b, as, 0, []string{}) + b = append(b, '\n') + return b } -func (h *developHandler) colorize(buf []byte, as attributes, level int, groups []string) []byte { +func (h *developHandler) colorize(b []byte, as attributes, l int, g []string) []byte { if h.opts.SortKeys { sort.Sort(as) } - keyColor := fgMagenta - padding := as.padding(keyColor) - + p := as.padding(fgMagenta) for _, a := range as { if h.opts.ReplaceAttr != nil { - a = h.opts.ReplaceAttr(groups, a) + a = h.opts.ReplaceAttr(g, a) } - key := cs(a.Key, keyColor) - val := a.Value.String() - var mark string + k := cs([]byte(a.Key), fgMagenta) + v := []byte(a.Value.String()) + m := []byte(" ") switch a.Value.Kind() { case slog.KindFloat64, slog.KindInt64, slog.KindUint64: - mark = cs("#", fgYellow) - val = cs(val, fgYellow) + m = cs([]byte("#"), fgYellow) + v = cs(v, fgYellow) case slog.KindBool: - mark = cs("#", fgRed) - val = cs(val, fgRed) + m = cs([]byte("#"), fgRed) + v = cs(v, fgRed) case slog.KindString: - if len(val) == 0 { - val = csf("empty", fgWhite) - } else if isURL(val) { - mark = cs("*", fgBlue) - val = cs(val, fgBlue) + if len(v) == 0 { + v = csf([]byte("empty"), fgWhite) + } else if h.isURL(v) { + m = cs([]byte("*"), fgBlue) + v = ul(cs(v, fgBlue)) } case slog.KindTime, slog.KindDuration: - mark = cs("@", fgCyan) - val = cs(val, fgCyan) + m = cs([]byte("@"), fgCyan) + v = cs(v, fgCyan) case slog.KindAny: - a := a.Value.Any() - err, isError := a.(error) - if isError { - mark = cs("E", fgRed) - val = csb(fmt.Sprintf(" %v ", err), fgBlack, bgRed) + any := a.Value.Any() + + err, isErr := any.(error) + if isErr { + m = cs([]byte("E"), fgRed) + v = h.formatError(err, l) break } - jsonBytes, err := json.Marshal(a) - if err != nil { + timeT, isTim := any.(*time.Time) + if isTim { + m = cs([]byte("@"), fgCyan) + v = cs([]byte(timeT.String()), fgCyan) break } - var decodedData interface{} - err = json.Unmarshal(jsonBytes, &decodedData) - if err != nil { + timeD, isTim := any.(*time.Duration) + if isTim { + m = cs([]byte("@"), fgCyan) + v = cs([]byte(timeD.String()), fgCyan) break } - switch decoded := decodedData.(type) { - case []interface{}: - mark = cs("S", fgGreen) - val = h.formatSlice(decoded, level) - case map[string]interface{}: - mark = cs("M", fgGreen) - val = h.formatMap(decoded, level) + at := reflect.TypeOf(any) + av := reflect.ValueOf(any) + ut, uv := h.reducePointerTypeValue(at, av) + + switch ut.Kind() { + case reflect.Slice: + m = cs([]byte("S"), fgGreen) + v = h.formatSlice(at, av, l) + case reflect.Map: + m = cs([]byte("M"), fgGreen) + v = h.formatMap(at, av, l) + case reflect.Struct: + m = cs([]byte("S"), fgYellow) + v = h.formatStruct(at, av, 0) + case reflect.Float32, reflect.Float64, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + m = cs([]byte("#"), fgYellow) + v = cs(atb(uv.Interface()), fgYellow) + case reflect.Bool: + m = cs([]byte("#"), fgRed) + v = cs(atb(uv.Bool()), fgRed) + case reflect.String: + v = []byte(uv.String()) } case slog.KindGroup: - mark = cs("G", fgGreen) - var groupAttrs attributes - groupAttrs = a.Value.Group() - groups = append(groups, a.Key) - val = fmt.Sprintf("%v\n%s", cs("group", fgGreen), h.colorize(nil, groupAttrs, level+1, groups)) + m = cs([]byte("G"), fgGreen) + var ga attributes + ga = a.Value.Group() + g = append(g, a.Key) + + v = cs([]byte("============"), fgGreen) + v = append(v, '\n') + v = append(v, h.colorize(nil, ga, l+1, g)...) } - buf = fmt.Appendf(buf, "%*v%1v %-*s : %s", level*2, "", mark, padding, key, val) + b = append(b, bytes.Repeat([]byte(" "), l*2)...) + b = append(b, m...) + b = append(b, ' ') + b = append(b, k...) + b = append(b, bytes.Repeat([]byte(" "), p-len(k))...) + b = append(b, ':') + b = append(b, ' ') + b = append(b, v...) if a.Value.Kind() != slog.KindGroup { - buf = append(buf, '\n') + b = append(b, '\n') } } - return buf + return b } -func isURL(s string) bool { - _, err := url.ParseRequestURI(s) +func (h *developHandler) isURL(u []byte) bool { + _, err := url.ParseRequestURI(string(u)) return err == nil } -func isSlice(s string) bool { - return strings.HasPrefix(s, "slice[") && strings.HasSuffix(s, "]") -} +func (h *developHandler) formatError(err error, l int) (b []byte) { + errs := make([][]byte, 0) + for err != nil { + unwrapErr, ok := err.(interface{ Unwrap() error }) + if !ok { + errs = append(errs, []byte(err.Error())) + break + } -func isMap(s string) bool { - return strings.HasPrefix(s, "map[") && strings.HasSuffix(s, "]") -} + ue := unwrapErr.Unwrap() + pe, ok := strings.CutSuffix(err.Error(), ue.Error()) + if ok { + errs = append(errs, []byte(pe)) + } + + err = ue + } -// Splitting is done by SliceElementDivider -func (h *developHandler) formatSlice(s []interface{}, l int) string { - if len(s) == 0 { - return fmt.Sprintf("%v %v", cs("0", fgYellow), cs("slice[]", fgGreen)) + b = append(b, ul(cs([]byte(errs[len(errs)-1]), fgRed))...) + d := len(strconv.Itoa(len(errs))) + for i, e := range errs { + tb := strconv.Itoa(i) + b = append(b, '\n') + b = append(b, bytes.Repeat([]byte(" "), l*2+4)...) + b = append(b, bytes.Repeat([]byte(" "), d-len(tb))...) + b = append(b, cs([]byte(tb), fgRed)...) + b = append(b, ':') + b = append(b, ' ') + b = append(b, ul(cs(e, fgRed))...) } - length := cs(strconv.Itoa(len(s)), fgYellow) - digits := len(strconv.Itoa(len(s))) - if digits > 3 { - digits = 3 + return b +} + +func (h *developHandler) formatSlice(st reflect.Type, sv reflect.Value, l int) (b []byte) { + ts := h.buildTypeString(st.String()) + st, sv = h.reducePointerTypeValue(st, sv) + + b = append(b, cs([]byte(strconv.Itoa(sv.Len())), fgBlue)...) + b = append(b, ' ') + b = append(b, ts...) + d := len(strconv.Itoa(sv.Len())) + if len(strconv.Itoa(int(h.opts.MaxSlicePrintSize))) < d { + d = len(strconv.Itoa(int(h.opts.MaxSlicePrintSize))) } - elementColor := fgBlue - res := fmt.Sprintf("%s %s", length, cs("slice[", fgGreen)) - for i, e := range s { + for i := 0; i < sv.Len(); i++ { if i == int(h.opts.MaxSlicePrintSize) { - res += fmt.Sprintf("\n%*v%*s %s%s", l*2+4, "", digits, "", cs("...", elementColor), cs("]", fgGreen)) + b = append(b, '\n') + b = append(b, bytes.Repeat([]byte(" "), l*2+4)...) + b = append(b, bytes.Repeat([]byte(" "), d+2)...) + b = append(b, cs([]byte("..."), fgBlue)...) + b = append(b, cs([]byte("]"), fgGreen)...) break } - res += fmt.Sprintf("\n%*v%*s: %s", l*2+4, "", digits, cs(strconv.Itoa(i), fgGreen), cs(fmt.Sprint(e), elementColor)) - if i == len(s)-1 { - res += fmt.Sprintf(" %s", cs("]", fgGreen)) + v := sv.Index(i) + t := v.Type() + + tb := strconv.Itoa(i) + b = append(b, '\n') + b = append(b, bytes.Repeat([]byte(" "), l*2+4)...) + b = append(b, bytes.Repeat([]byte(" "), d-len(tb))...) + b = append(b, cs([]byte(tb), fgGreen)...) + b = append(b, ':') + b = append(b, ' ') + b = append(b, h.elementType(t, v, l)...) + + } + + return b +} + +func (h *developHandler) formatMap(st reflect.Type, sv reflect.Value, l int) (b []byte) { + ts := h.buildTypeString(st.String()) + st, sv = h.reducePointerTypeValue(st, sv) + + p := h.mapKeyPadding(sv, fgGreen) + b = append(b, cs([]byte(strconv.Itoa(sv.Len())), fgBlue)...) + b = append(b, ' ') + b = append(b, ts...) + sk := h.sortMapKeys(sv) + for _, k := range sk { + v := sv.MapIndex(k) + v = h.reducePointerValue(v) + k = h.reducePointerValue(k) + + tb := cs(atb(k.Interface()), fgGreen) + b = append(b, '\n') + b = append(b, bytes.Repeat([]byte(" "), l*2+4)...) + b = append(b, tb...) + b = append(b, bytes.Repeat([]byte(" "), p-len(tb))...) + b = append(b, ':') + b = append(b, ' ') + b = append(b, h.elementType(v.Type(), v, l)...) + } + + return b +} + +func (h *developHandler) formatStruct(st reflect.Type, sv reflect.Value, l int) (b []byte) { + b = h.buildTypeString(st.String()) + + st, sv = h.reducePointerTypeValue(st, sv) + p := h.structKeyPadding(sv, fgGreen) + + for i := 0; i < sv.NumField(); i++ { + v := sv.Field(i) + t := v.Type() + + tb := cs([]byte(sv.Type().Field(i).Name), fgGreen) + b = append(b, '\n') + b = append(b, bytes.Repeat([]byte(" "), l*2+4)...) + b = append(b, tb...) + b = append(b, bytes.Repeat([]byte(" "), p-len(tb))...) + b = append(b, ':') + b = append(b, ' ') + b = append(b, h.elementType(t, v, l)...) + } + + return b +} + +func (h *developHandler) elementType(t reflect.Type, v reflect.Value, l int) (b []byte) { + switch v.Kind() { + case reflect.Slice: + b = h.formatSlice(t, v, l+1) + case reflect.Map: + b = h.formatMap(t, v, l+1) + case reflect.Struct: + b = h.formatStruct(t, v, l+1) + case reflect.Pointer: + switch v.Elem().Kind() { + case reflect.Slice: + b = h.formatSlice(t, v, l+1) + case reflect.Map: + b = h.formatMap(t, v, l+1) + case reflect.Struct: + b = h.formatStruct(t, v, l+1) } + default: + b = atb(v.Interface()) } - return res + return b } -// Splitting is done by SliceElementDivider, -func (h *developHandler) formatMap(s map[string]interface{}, level int) string { - if len(s) == 0 { - return fmt.Sprintf("%v %v", cs("0", fgYellow), cs("map[]", fgGreen)) +func (h *developHandler) buildTypeString(ts string) (b []byte) { + t := []byte(ts) + + for len(t) > 0 { + switch t[0] { + case '*': + b = append(b, cs([]byte{t[0]}, fgRed)...) + case '[', ']': + b = append(b, cs([]byte{t[0]}, fgGreen)...) + default: + b = append(b, cs([]byte{t[0]}, fgYellow)...) + } + + t = t[1:] } - var padding int - for key := range s { - color := len(cs(key, fgGreen)) - if color > padding { - padding = color + return b +} + +func (h *developHandler) sortMapKeys(rv reflect.Value) []reflect.Value { + ks := make([]reflect.Value, 0, rv.Len()) + for _, k := range rv.MapKeys() { + ks = append(ks, k) + } + + sort.Slice(ks, func(i, j int) bool { + return fmt.Sprint(ks[i].Interface()) < fmt.Sprint(ks[j].Interface()) + }) + + return ks +} + +func (h *developHandler) mapKeyPadding(rv reflect.Value, fgColor foregroundColor) (p int) { + for _, k := range rv.MapKeys() { + k = h.reducePointerValue(k) + c := len(cs(atb(k.Interface()), fgColor)) + if c > p { + p = c } } - sortedKeys := sortDataMap(s) - length := cs(strconv.Itoa(len(sortedKeys)), fgYellow) - res := fmt.Sprintf("%s %s", length, cs("map[", fgGreen)) - for i, key := range sortedKeys { - res += fmt.Sprintf("\n%*v%-*s : %s", level*2+4, "", padding, cs(key, fgGreen), cs(fmt.Sprint(s[key]), fgBlue)) - if i == len(sortedKeys)-1 { - res += fmt.Sprintf(" %s", cs("]", fgGreen)) + return p +} + +func (h *developHandler) structKeyPadding(sv reflect.Value, fgColor foregroundColor) (p int) { + st := sv.Type() + for i := 0; i < sv.NumField(); i++ { + c := len(cs([]byte(st.Field(i).Name), fgColor)) + if c > p { + p = c } } - return res + return p } -func sortDataMap(data map[string]interface{}) []string { - keys := make([]string, 0, len(data)) - for key := range data { - keys = append(keys, key) +func (h *developHandler) reducePointerValue(v reflect.Value) reflect.Value { + for v.Kind() == reflect.Pointer { + v = v.Elem() } - sort.Strings(keys) + return v +} + +func (h *developHandler) reducePointerTypeValue(t reflect.Type, v reflect.Value) (reflect.Type, reflect.Value) { + for t.Kind() == reflect.Pointer { + v = v.Elem() + t = v.Type() + } + + return t, v +} - return keys +// Any to []byte using fmt.Sprint +func atb(a any) []byte { + return []byte(fmt.Sprint(a)) } diff --git a/devslog_test.go b/devslog_test.go index 70429f7..da9006d 100644 --- a/devslog_test.go +++ b/devslog_test.go @@ -11,7 +11,67 @@ import ( "time" ) -func Test_NewHandlerDefaults(t *testing.T) { +func Test_NewHandler(t *testing.T) { + test_NewHandlerDefaults(t) + test_NewHandlerWithOptions(t) + test_NewHandlerWithNilOptions(t) + test_NewHandlerWithNilSlogHandlerOptions(t) +} + +func Test_Methods(t *testing.T) { + test_Enabled(t) + test_WithGroup(t) + test_WithGroupEmpty(t) + test_WithAttrs(t) + test_WithAttrsEmpty(t) +} + +func Test_Levels(t *testing.T) { + test_LevelMessageDebug(t) + test_LevelMessageInfo(t) + test_LevelMessageWarn(t) + test_LevelMessageError(t) +} + +func Test_GroupsAndAttributes(t *testing.T) { + test_WithGroups(t) + test_WithGroupsEmpty(t) + test_WithAttributes(t) +} + +func Test_SourceAndReplace(t *testing.T) { + test_Source(t) + test_ReplaceLevelAttributes(t) +} + +func Test_Types(t *testing.T) { + slogOpts := &slog.HandlerOptions{ + AddSource: false, + Level: slog.LevelDebug, + ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { return a }, + } + + opts := &Options{ + HandlerOptions: slogOpts, + MaxSlicePrintSize: 4, + SortKeys: true, + TimeFormat: "[]", + } + + test_String(t, opts) + test_IntFloat(t, opts) + test_Bool(t, opts) + test_Time(t, opts) + test_Error(t, opts) + test_Slice(t, opts) + test_SliceBig(t, opts) + test_Map(t, opts) + test_MapOfPointers(t, opts) + test_Struct(t, opts) + test_Group(t, opts) +} + +func test_NewHandlerDefaults(t *testing.T) { opts := &Options{ HandlerOptions: &slog.HandlerOptions{}, } @@ -38,7 +98,7 @@ func Test_NewHandlerDefaults(t *testing.T) { } } -func Test_NewHandlerWithOptions(t *testing.T) { +func test_NewHandlerWithOptions(t *testing.T) { handlerOpts := &Options{ HandlerOptions: &slog.HandlerOptions{Level: slog.LevelWarn}, MaxSlicePrintSize: 10, @@ -59,7 +119,7 @@ func Test_NewHandlerWithOptions(t *testing.T) { } } -func Test_NewHandlerWithNilOptions(t *testing.T) { +func test_NewHandlerWithNilOptions(t *testing.T) { h := NewHandler(nil, nil) if h.opts.HandlerOptions == nil || h.opts.HandlerOptions.Level != slog.LevelInfo { @@ -75,7 +135,24 @@ func Test_NewHandlerWithNilOptions(t *testing.T) { } } -func Test_Enabled(t *testing.T) { +func test_NewHandlerWithNilSlogHandlerOptions(t *testing.T) { + opts := &Options{} + h := NewHandler(nil, opts) + + if h.opts.HandlerOptions == nil || h.opts.HandlerOptions.Level != slog.LevelInfo { + t.Errorf("Expected HandlerOptions to be initialized with default level") + } + + if h.opts.MaxSlicePrintSize != 50 { + t.Errorf("Expected MaxSlicePrintSize to be initialized with default value") + } + + if h.out != nil { + t.Errorf("Expected writer to be nil") + } +} + +func test_Enabled(t *testing.T) { h := NewHandler(nil, nil) ctx := context.Background() @@ -88,7 +165,7 @@ func Test_Enabled(t *testing.T) { } } -func Test_WithGroup(t *testing.T) { +func test_WithGroup(t *testing.T) { h := NewHandler(nil, nil) h2 := h.WithGroup("myGroup") @@ -97,7 +174,7 @@ func Test_WithGroup(t *testing.T) { } } -func Test_WithGroupEmpty(t *testing.T) { +func test_WithGroupEmpty(t *testing.T) { h := NewHandler(nil, nil) h2 := h.WithGroup("") @@ -106,7 +183,7 @@ func Test_WithGroupEmpty(t *testing.T) { } } -func Test_WithAttrs(t *testing.T) { +func test_WithAttrs(t *testing.T) { h := NewHandler(nil, nil) h2 := h.WithAttrs([]slog.Attr{slog.String("key", "value")}) @@ -115,7 +192,7 @@ func Test_WithAttrs(t *testing.T) { } } -func Test_WithAttrsEmpty(t *testing.T) { +func test_WithAttrsEmpty(t *testing.T) { h := NewHandler(nil, nil) h2 := h.WithAttrs([]slog.Attr{}) @@ -124,111 +201,7 @@ func Test_WithAttrsEmpty(t *testing.T) { } } -func Test_IsURL(t *testing.T) { - urlString := "https://www.example.com" - if !isURL(urlString) { - t.Errorf("Expected URL to be recognized as URL: %s", urlString) - } - - nonURLString := "not-a-valid-url" - if isURL(nonURLString) { - t.Errorf("Expected non-URL string to not be recognized as URL: %s", nonURLString) - } -} - -func Test_IsMap(t *testing.T) { - mapString := "map[key:value]" - if !isMap(mapString) { - t.Errorf("Expected string to be recognized as map: %s", mapString) - } - - nonMapString := "not-a-valid-map" - if isMap(nonMapString) { - t.Errorf("Expected non-map string to not be recognized as map: %s", nonMapString) - } -} - -func Test_IsSlice(t *testing.T) { - sliceString := "slice[1 2 3]" - if !isSlice(sliceString) { - t.Errorf("Expected string to be recognized as slice: %s", sliceString) - } - - nonSliceString := "not-a-valid-slice" - if isSlice(nonSliceString) { - t.Errorf("Expected non-slice string to not be recognized as slice: %s", nonSliceString) - } -} - -func Test_ArrayString(t *testing.T) { - h := NewHandler(nil, nil) - data := []interface{}{"apple", "ba na na"} - expected := "\x1b[33m2\x1b[0m \x1b[32mslice[\x1b[0m\n \x1b[32m0\x1b[0m: \x1b[34mapple\x1b[0m\n \x1b[32m1\x1b[0m: \x1b[34mba na na\x1b[0m \x1b[32m]\x1b[0m" - - result := h.formatSlice(data, 0) - - if result != expected { - t.Errorf("\nExpected:\n%s\nGot:\n%s\nExpected:\n%[1]q\nGot:\n%[2]q", expected, result) - } -} - -func Test_ArrayStringEmpty(t *testing.T) { - h := NewHandler(nil, nil) - data := make([]interface{}, 0) - expected := "\x1b[33m0\x1b[0m \x1b[32mslice[]\x1b[0m" - - result := h.formatSlice(data, 0) - - if result != expected { - t.Errorf("\nExpected:\n%s\nGot:\n%s\nExpected:\n%[1]q\nGot:\n%[2]q", expected, result) - } -} - -func Test_ArrayStringBig(t *testing.T) { - opts := &Options{ - MaxSlicePrintSize: 4, - } - - h := NewHandler(nil, opts) - slice := make([]interface{}, 1000) - for i := 0; i < 1000; i++ { - slice[i] = i + 1 - } - - expected := "\x1b[33m1000\x1b[0m \x1b[32mslice[\x1b[0m\n \x1b[32m0\x1b[0m: \x1b[34m1\x1b[0m\n \x1b[32m1\x1b[0m: \x1b[34m2\x1b[0m\n \x1b[32m2\x1b[0m: \x1b[34m3\x1b[0m\n \x1b[32m3\x1b[0m: \x1b[34m4\x1b[0m\n \x1b[34m...\x1b[0m\x1b[32m]\x1b[0m" - - result := h.formatSlice(slice, 0) - - if result != expected { - t.Errorf("\nExpected:\n%s\nGot:\n%s\nExpected:\n%[1]q\nGot:\n%[2]q", expected, result) - } -} - -func Test_MapString(t *testing.T) { - h := NewHandler(nil, nil) - data := map[string]interface{}{"a": "1", "b": "2"} - expected := "\x1b[33m2\x1b[0m \x1b[32mmap[\x1b[0m\n \x1b[32ma\x1b[0m : \x1b[34m1\x1b[0m\n \x1b[32mb\x1b[0m : \x1b[34m2\x1b[0m \x1b[32m]\x1b[0m" - - result := h.formatMap(data, 0) - - if result != expected { - t.Errorf("\nExpected:\n%s\nGot:\n%s\nExpected:\n%[1]q\nGot:\n%[2]q", expected, result) - } -} - -func Test_MapStringEmpty(t *testing.T) { - h := NewHandler(nil, nil) - data := make(map[string]interface{}, 0) - expected := "\x1b[33m0\x1b[0m \x1b[32mmap[]\x1b[0m" - - result := h.formatMap(data, 0) - - if result != expected { - t.Errorf("\nExpected:\n%s\nGot:\n%s\nExpected:\n%[1]q\nGot:\n%[2]q", expected, result) - } -} - -func Test_LevelMessageDebug(t *testing.T) { +func test_LevelMessageDebug(t *testing.T) { h := NewHandler(nil, nil) buf := make([]byte, 0) record := &slog.Record{ @@ -238,7 +211,7 @@ func Test_LevelMessageDebug(t *testing.T) { buf = h.levelMessage(buf, record) - expected := "\x1b[30m\x1b[44m DEBUG \x1b[0m \x1b[34mDebug message\x1b[0m\n" + expected := "\x1b[44m\x1b[30m DEBUG \x1b[0m \x1b[34mDebug message\x1b[0m\n" result := string(buf) if result != expected { @@ -246,7 +219,7 @@ func Test_LevelMessageDebug(t *testing.T) { } } -func Test_LevelMessageInfo(t *testing.T) { +func test_LevelMessageInfo(t *testing.T) { h := NewHandler(nil, nil) buf := make([]byte, 0) record := &slog.Record{ @@ -256,7 +229,7 @@ func Test_LevelMessageInfo(t *testing.T) { buf = h.levelMessage(buf, record) - expected := "\x1b[30m\x1b[42m INFO \x1b[0m \x1b[32mInfo message\x1b[0m\n" + expected := "\x1b[42m\x1b[30m INFO \x1b[0m \x1b[32mInfo message\x1b[0m\n" result := string(buf) if result != expected { @@ -264,7 +237,7 @@ func Test_LevelMessageInfo(t *testing.T) { } } -func Test_LevelMessageWarn(t *testing.T) { +func test_LevelMessageWarn(t *testing.T) { h := NewHandler(nil, nil) buf := make([]byte, 0) record := &slog.Record{ @@ -274,7 +247,7 @@ func Test_LevelMessageWarn(t *testing.T) { buf = h.levelMessage(buf, record) - expected := "\x1b[30m\x1b[43m WARN \x1b[0m \x1b[33mWarning message\x1b[0m\n" + expected := "\x1b[43m\x1b[30m WARN \x1b[0m \x1b[33mWarning message\x1b[0m\n" result := string(buf) if result != expected { @@ -282,7 +255,7 @@ func Test_LevelMessageWarn(t *testing.T) { } } -func Test_LevelMessageError(t *testing.T) { +func test_LevelMessageError(t *testing.T) { h := NewHandler(nil, nil) buf := make([]byte, 0) record := &slog.Record{ @@ -292,7 +265,7 @@ func Test_LevelMessageError(t *testing.T) { buf = h.levelMessage(buf, record) - expected := "\x1b[30m\x1b[41m ERROR \x1b[0m \x1b[31mError message\x1b[0m\n" + expected := "\x1b[41m\x1b[30m ERROR \x1b[0m \x1b[31mError message\x1b[0m\n" result := string(buf) if result != expected { @@ -309,11 +282,11 @@ type MockWriter struct { WrittenData []byte } -func Test_WholeOutput(t *testing.T) { +func test_Source(t *testing.T) { w := &MockWriter{} slogOpts := &slog.HandlerOptions{ - AddSource: false, + AddSource: true, Level: slog.LevelDebug, ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { return a }, } @@ -325,85 +298,74 @@ func Test_WholeOutput(t *testing.T) { TimeFormat: "[15:06]", } - logger := slog.New(NewHandler(w, opts).WithAttrs([]slog.Attr{slog.String("attr", "string")}).WithGroup("with_group")) + logger := slog.New(NewHandler(w, opts)) + + timeString := csf([]byte(time.Now().Format("[15:06]")), fgWhite) + _, filename, l, _ := runtime.Caller(0) + logger.Info("message") + + expected := fmt.Sprintf("%1s \x1b[34m@@@\x1b[0m \x1b[4m\x1b[33m%2s\x1b[0m\x1b[0m:\x1b[31m%v\x1b[0m\n\x1b[42m\x1b[30m INFO \x1b[0m \x1b[32mmessage\x1b[0m\n\n", timeString, filename, l+1) + + if !bytes.Equal(w.WrittenData, []byte(expected)) { + t.Errorf("\nExpected:\n%s\nGot:\n%s\nExpected:\n%[1]q\nGot:\n%[2]q", expected, w.WrittenData) + } +} + +func test_WithGroups(t *testing.T) { + w := &MockWriter{} - mapString := map[string]string{ - "apple": "pear", - "ba na na": "man go", + slogOpts := &slog.HandlerOptions{ + AddSource: false, + Level: slog.LevelDebug, } - sliceSmall := []string{"dsa", "ba na na"} - sliceBig := make([]int, 1000) - for i := 0; i < 1000; i++ { - sliceBig[i] = i + 1 + opts := &Options{ + HandlerOptions: slogOpts, + MaxSlicePrintSize: 4, + SortKeys: true, + TimeFormat: "[]", } - emptySlice := make([]int, 0) - emptyMap := make(map[int]int, 0) + logger := slog.New(NewHandler(w, opts).WithGroup("test_group")) - timeString := csf(time.Now().Format("[15:06]"), fgWhite) - logger.Info( - "My INFO message", - slog.String("test_string", "some string"), - slog.String("empty", ""), - slog.String("url", "https://go.dev/"), - slog.Any("boolean", true), - slog.Any("time", time.Date(2012, time.March, 28, 0, 0, 0, 0, time.UTC)), - slog.Any("duration", time.Second), - slog.Any("map", mapString), - slog.Any("empty_map", emptyMap), - slog.Any("slice", sliceSmall), - slog.Any("slice_big", sliceBig), - slog.Any("empty_slice", emptySlice), - slog.Group("my_group", - slog.Any("int", 1), - slog.Any("float", 1.21), - ), + logger.Info("My INFO message", + slog.Any("a", "1"), ) - expected := fmt.Sprintf("%[1]v \x1b[30m\x1b[42m INFO \x1b[0m \x1b[32mMy INFO message\x1b[0m\n \x1b[35mattr\x1b[0m : string\n\x1b[32mG\x1b[0m \x1b[35mwith_group\x1b[0m : \x1b[32mgroup\x1b[0m\n \x1b[31m#\x1b[0m \x1b[35mboolean\x1b[0m : \x1b[31mtrue\x1b[0m\n \x1b[36m@\x1b[0m \x1b[35mduration\x1b[0m : \x1b[36m1s\x1b[0m\n \x1b[35mempty\x1b[0m : \x1b[37m\x1b[2mempty\x1b[0m\n \x1b[32mM\x1b[0m \x1b[35mempty_map\x1b[0m : \x1b[33m0\x1b[0m \x1b[32mmap[]\x1b[0m\n \x1b[32mS\x1b[0m \x1b[35mempty_slice\x1b[0m : \x1b[33m0\x1b[0m \x1b[32mslice[]\x1b[0m\n \x1b[32mM\x1b[0m \x1b[35mmap\x1b[0m : \x1b[33m2\x1b[0m \x1b[32mmap[\x1b[0m\n \x1b[32mapple\x1b[0m : \x1b[34mpear\x1b[0m\n \x1b[32mba na na\x1b[0m : \x1b[34mman go\x1b[0m \x1b[32m]\x1b[0m\n \x1b[32mS\x1b[0m \x1b[35mslice\x1b[0m : \x1b[33m2\x1b[0m \x1b[32mslice[\x1b[0m\n \x1b[32m0\x1b[0m: \x1b[34mdsa\x1b[0m\n \x1b[32m1\x1b[0m: \x1b[34mba na na\x1b[0m \x1b[32m]\x1b[0m\n \x1b[32mS\x1b[0m \x1b[35mslice_big\x1b[0m : \x1b[33m1000\x1b[0m \x1b[32mslice[\x1b[0m\n \x1b[32m0\x1b[0m: \x1b[34m1\x1b[0m\n \x1b[32m1\x1b[0m: \x1b[34m2\x1b[0m\n \x1b[32m2\x1b[0m: \x1b[34m3\x1b[0m\n \x1b[32m3\x1b[0m: \x1b[34m4\x1b[0m\n \x1b[34m...\x1b[0m\x1b[32m]\x1b[0m\n \x1b[35mtest_string\x1b[0m : some string\n \x1b[36m@\x1b[0m \x1b[35mtime\x1b[0m : \x1b[36m2012-03-28 00:00:00 +0000 UTC\x1b[0m\n \x1b[34m*\x1b[0m \x1b[35murl\x1b[0m : \x1b[34mhttps://go.dev/\x1b[0m\n \x1b[32mG\x1b[0m \x1b[35mmy_group\x1b[0m : \x1b[32mgroup\x1b[0m\n \x1b[33m#\x1b[0m \x1b[35mfloat\x1b[0m : \x1b[33m1.21\x1b[0m\n \x1b[33m#\x1b[0m \x1b[35mint\x1b[0m : \x1b[33m1\x1b[0m\n\n", timeString) + expected := fmt.Sprint("\x1b[2m\x1b[37m[]\x1b[0m \x1b[42m\x1b[30m INFO \x1b[0m \x1b[32mMy INFO message\x1b[0m\n\x1b[32mG\x1b[0m \x1b[35mtest_group\x1b[0m: \x1b[32m============\x1b[0m\n \x1b[35ma\x1b[0m: 1\n\n") if !bytes.Equal(w.WrittenData, []byte(expected)) { t.Errorf("\nExpected:\n%s\nGot:\n%s\nExpected:\n%[1]q\nGot:\n%[2]q", expected, w.WrittenData) } } -func Test_EmptyLogs(t *testing.T) { +func test_WithGroupsEmpty(t *testing.T) { w := &MockWriter{} slogOpts := &slog.HandlerOptions{ - AddSource: true, - Level: slog.LevelDebug, - ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { return a }, + AddSource: false, + Level: slog.LevelDebug, } opts := &Options{ HandlerOptions: slogOpts, MaxSlicePrintSize: 4, SortKeys: true, - TimeFormat: "[15:06]", + TimeFormat: "[]", } - logger := slog.New(NewHandler(w, opts)) + logger := slog.New(NewHandler(w, opts).WithGroup("test_group")) - timeString := csf(time.Now().Format("[15:06]"), fgWhite) - _, filename, l1, _ := runtime.Caller(0) - logger.Debug("My DEBUG message") - _, _, l2, _ := runtime.Caller(0) logger.Info("My INFO message") - _, _, l3, _ := runtime.Caller(0) - logger.Warn("My WARN message") - _, _, l4, _ := runtime.Caller(0) - logger.Error("My ERROR message") - expected := fmt.Sprintf("%[1]v \x1b[34m@@@\x1b[0m \x1b[4m\x1b[33m%[2]s\x1b[0m\x1b[0m:\x1b[31m%[3]v\x1b[0m\n\x1b[30m\x1b[44m DEBUG \x1b[0m \x1b[34mMy DEBUG message\x1b[0m\n\n%[1]v \x1b[34m@@@\x1b[0m \x1b[4m\x1b[33m%[2]v\x1b[0m\x1b[0m:\x1b[31m%[4]v\x1b[0m\n\x1b[30m\x1b[42m INFO \x1b[0m \x1b[32mMy INFO message\x1b[0m\n\n%[1]v \x1b[34m@@@\x1b[0m \x1b[4m\x1b[33m%[2]v\x1b[0m\x1b[0m:\x1b[31m%[5]v\x1b[0m\n\x1b[30m\x1b[43m WARN \x1b[0m \x1b[33mMy WARN message\x1b[0m\n\n%[1]v \x1b[34m@@@\x1b[0m \x1b[4m\x1b[33m%[2]v\x1b[0m\x1b[0m:\x1b[31m%[6]v\x1b[0m\n\x1b[30m\x1b[41m ERROR \x1b[0m \x1b[31mMy ERROR message\x1b[0m\n\n", timeString, filename, l1+1, l2+1, l3+1, l4+1) + expected := fmt.Sprint("\x1b[2m\x1b[37m[]\x1b[0m \x1b[42m\x1b[30m INFO \x1b[0m \x1b[32mMy INFO message\x1b[0m\n\n") if !bytes.Equal(w.WrittenData, []byte(expected)) { t.Errorf("\nExpected:\n%s\nGot:\n%s\nExpected:\n%[1]q\nGot:\n%[2]q", expected, w.WrittenData) } } -func Test_WithGroups(t *testing.T) { +func test_WithAttributes(t *testing.T) { w := &MockWriter{} slogOpts := &slog.HandlerOptions{ @@ -415,15 +377,15 @@ func Test_WithGroups(t *testing.T) { HandlerOptions: slogOpts, MaxSlicePrintSize: 4, SortKeys: true, - TimeFormat: "[15:06]", + TimeFormat: "[]", } - logger := slog.New(NewHandler(w, opts).WithGroup("test_group")) + as := []slog.Attr{slog.Any("a", "1")} + logger := slog.New(NewHandler(w, opts).WithAttrs(as)) - timeString := csf(time.Now().Format("[15:06]"), fgWhite) logger.Info("My INFO message") - expected := fmt.Sprintf("%[1]v \x1b[30m\x1b[42m INFO \x1b[0m \x1b[32mMy INFO message\x1b[0m\n\n", timeString) + expected := fmt.Sprint("\x1b[2m\x1b[37m[]\x1b[0m \x1b[42m\x1b[30m INFO \x1b[0m \x1b[32mMy INFO message\x1b[0m\n \x1b[35ma\x1b[0m: 1\n\n") if !bytes.Equal(w.WrittenData, []byte(expected)) { t.Errorf("\nExpected:\n%s\nGot:\n%s\nExpected:\n%[1]q\nGot:\n%[2]q", expected, w.WrittenData) @@ -440,7 +402,7 @@ const ( LevelEmergency = slog.Level(12) ) -func Test_ReplaceLevelAttributes(t *testing.T) { +func test_ReplaceLevelAttributes(t *testing.T) { w := &MockWriter{} slogOpts := &slog.HandlerOptions{ @@ -458,7 +420,7 @@ func Test_ReplaceLevelAttributes(t *testing.T) { logger := slog.New(NewHandler(w, opts)) - timeString := csf(time.Now().Format("[15:06]"), fgWhite) + timeString := csf([]byte(time.Now().Format("[15:06]")), fgWhite) ctx := context.Background() logger.Log(ctx, LevelEmergency, "missing pilots") logger.Error("failed to start engines", "err", "missing fuel") @@ -468,7 +430,7 @@ func Test_ReplaceLevelAttributes(t *testing.T) { logger.Debug("starting background job") logger.Log(ctx, LevelTrace, "button clicked") - expected := fmt.Sprintf("%[1]v \x1b[30m\x1b[41m EMERGENCY \x1b[0m \x1b[31mmissing pilots\x1b[0m\n \x1b[35msev\x1b[0m : EMERGENCY\n\n%[1]v \x1b[30m\x1b[41m ERROR \x1b[0m \x1b[31mfailed to start engines\x1b[0m\n \x1b[35merr\x1b[0m : missing fuel\n \x1b[35msev\x1b[0m : ERROR\n\n%[1]v \x1b[30m\x1b[43m WARNING \x1b[0m \x1b[33mfalling back to default value\x1b[0m\n \x1b[35msev\x1b[0m : WARNING\n\n%[1]v \x1b[30m\x1b[42m NOTICE \x1b[0m \x1b[32mall systems are running\x1b[0m\n \x1b[35msev\x1b[0m : NOTICE\n\n%[1]v \x1b[30m\x1b[42m INFO \x1b[0m \x1b[32minitiating launch\x1b[0m\n \x1b[35msev\x1b[0m : INFO\n\n%[1]v \x1b[30m\x1b[44m DEBUG \x1b[0m \x1b[34mstarting background job\x1b[0m\n \x1b[35msev\x1b[0m : DEBUG\n\n", timeString) + expected := fmt.Sprintf("%[1]s \x1b[41m\x1b[30m EMERGENCY \x1b[0m \x1b[31mmissing pilots\x1b[0m\n \x1b[35msev\x1b[0m: EMERGENCY\n\n%[1]s \x1b[41m\x1b[30m ERROR \x1b[0m \x1b[31mfailed to start engines\x1b[0m\n \x1b[35merr\x1b[0m: missing fuel\n \x1b[35msev\x1b[0m: ERROR\n\n%[1]s \x1b[43m\x1b[30m WARNING \x1b[0m \x1b[33mfalling back to default value\x1b[0m\n \x1b[35msev\x1b[0m: WARNING\n\n%[1]s \x1b[42m\x1b[30m NOTICE \x1b[0m \x1b[32mall systems are running\x1b[0m\n \x1b[35msev\x1b[0m: NOTICE\n\n%[1]s \x1b[42m\x1b[30m INFO \x1b[0m \x1b[32minitiating launch\x1b[0m\n \x1b[35msev\x1b[0m: INFO\n\n%[1]s \x1b[44m\x1b[30m DEBUG \x1b[0m \x1b[34mstarting background job\x1b[0m\n \x1b[35msev\x1b[0m: DEBUG\n\n", timeString) if !bytes.Equal(w.WrittenData, []byte(expected)) { t.Errorf("\nExpected:\n%s\nGot:\n%s\nExpected:\n%[1]q\nGot:\n%[2]q", expected, w.WrittenData) @@ -507,3 +469,228 @@ func replaceAttributes(groups []string, a slog.Attr) slog.Attr { return a } + +func test_String(t *testing.T, o *Options) { + w := &MockWriter{} + logger := slog.New(NewHandler(w, o)) + + s := "string" + + logger.Info("msg", + slog.Any("s", s), + slog.Any("sp", &s), + slog.Any("empty", ""), + slog.Any("url", "https://go.dev/"), + ) + + expected := []byte("\x1b[2m\x1b[37m[]\x1b[0m \x1b[42m\x1b[30m INFO \x1b[0m \x1b[32mmsg\x1b[0m\n \x1b[35mempty\x1b[0m: \x1b[2m\x1b[37mempty\x1b[0m\n \x1b[35ms\x1b[0m : string\n \x1b[35msp\x1b[0m : string\n\x1b[34m*\x1b[0m \x1b[35murl\x1b[0m : \x1b[4m\x1b[34mhttps://go.dev/\x1b[0m\x1b[0m\n\n") + + if !bytes.Equal(w.WrittenData, expected) { + t.Errorf("\nExpected:\n%s\nGot:\n%s\nExpected:\n%[1]q\nGot:\n%[2]q", expected, w.WrittenData) + } +} + +func test_IntFloat(t *testing.T, o *Options) { + w := &MockWriter{} + logger := slog.New(NewHandler(w, o)) + + i := 1 + f := 1.21 + logger.Info("msg", + slog.Any("i", i), + slog.Any("f", f), + slog.Any("ip", &i), + slog.Any("fp", &f), + ) + + expected := []byte("\x1b[2m\x1b[37m[]\x1b[0m \x1b[42m\x1b[30m INFO \x1b[0m \x1b[32mmsg\x1b[0m\n\x1b[33m#\x1b[0m \x1b[35mf\x1b[0m : \x1b[33m1.21\x1b[0m\n\x1b[33m#\x1b[0m \x1b[35mfp\x1b[0m: \x1b[33m1.21\x1b[0m\n\x1b[33m#\x1b[0m \x1b[35mi\x1b[0m : \x1b[33m1\x1b[0m\n\x1b[33m#\x1b[0m \x1b[35mip\x1b[0m: \x1b[33m1\x1b[0m\n\n") + + if !bytes.Equal(w.WrittenData, expected) { + t.Errorf("\nExpected:\n%s\nGot:\n%s\nExpected:\n%[1]q\nGot:\n%[2]q", expected, w.WrittenData) + } +} + +func test_Bool(t *testing.T, o *Options) { + w := &MockWriter{} + logger := slog.New(NewHandler(w, o)) + + b := true + + logger.Info("msg", + slog.Any("b", b), + slog.Any("bp", &b), + ) + + expected := []byte("\x1b[2m\x1b[37m[]\x1b[0m \x1b[42m\x1b[30m INFO \x1b[0m \x1b[32mmsg\x1b[0m\n\x1b[31m#\x1b[0m \x1b[35mb\x1b[0m : \x1b[31mtrue\x1b[0m\n\x1b[31m#\x1b[0m \x1b[35mbp\x1b[0m: \x1b[31mtrue\x1b[0m\n\n") + + if !bytes.Equal(w.WrittenData, expected) { + t.Errorf("\nExpected:\n%s\nGot:\n%s\nExpected:\n%[1]q\nGot:\n%[2]q", expected, w.WrittenData) + } +} + +func test_Time(t *testing.T, o *Options) { + w := &MockWriter{} + logger := slog.New(NewHandler(w, o)) + + timeT := time.Date(2012, time.March, 28, 0, 0, 0, 0, time.UTC) + timeE := time.Date(2023, time.August, 15, 12, 0, 0, 0, time.UTC) + timeD := timeE.Sub(timeT) + + logger.Info("msg", + slog.Any("t", timeT), + slog.Any("tp", &timeT), + slog.Any("d", timeD), + slog.Any("tp", &timeD), + ) + + expected := []byte("\x1b[2m\x1b[37m[]\x1b[0m \x1b[42m\x1b[30m INFO \x1b[0m \x1b[32mmsg\x1b[0m\n\x1b[36m@\x1b[0m \x1b[35md\x1b[0m : \x1b[36m99780h0m0s\x1b[0m\n\x1b[36m@\x1b[0m \x1b[35mt\x1b[0m : \x1b[36m2012-03-28 00:00:00 +0000 UTC\x1b[0m\n\x1b[36m@\x1b[0m \x1b[35mtp\x1b[0m: \x1b[36m2012-03-28 00:00:00 +0000 UTC\x1b[0m\n\x1b[36m@\x1b[0m \x1b[35mtp\x1b[0m: \x1b[36m99780h0m0s\x1b[0m\n\n") + + if !bytes.Equal(w.WrittenData, expected) { + t.Errorf("\nExpected:\n%s\nGot:\n%s\nExpected:\n%[1]q\nGot:\n%[2]q", expected, w.WrittenData) + } +} + +func test_Error(t *testing.T, o *Options) { + w := &MockWriter{} + logger := slog.New(NewHandler(w, o)) + + e := fmt.Errorf("broken") + e = fmt.Errorf("err 1: %w", e) + e = fmt.Errorf("err 2: %w", e) + + logger.Info("msg", + slog.Any("e", e), + ) + + expected := []byte("\x1b[2m\x1b[37m[]\x1b[0m \x1b[42m\x1b[30m INFO \x1b[0m \x1b[32mmsg\x1b[0m\n\x1b[31mE\x1b[0m \x1b[35me\x1b[0m: \x1b[4m\x1b[31mbroken\x1b[0m\x1b[0m\n \x1b[31m0\x1b[0m: \x1b[4m\x1b[31merr 2: \x1b[0m\x1b[0m\n \x1b[31m1\x1b[0m: \x1b[4m\x1b[31merr 1: \x1b[0m\x1b[0m\n \x1b[31m2\x1b[0m: \x1b[4m\x1b[31mbroken\x1b[0m\x1b[0m\n\n") + + if !bytes.Equal(w.WrittenData, expected) { + t.Errorf("\nExpected:\n%s\nGot:\n%s\nExpected:\n%[1]q\nGot:\n%[2]q", expected, w.WrittenData) + } +} + +func test_Slice(t *testing.T, o *Options) { + w := &MockWriter{} + logger := slog.New(NewHandler(w, o)) + + s := []string{"apple", "ba na na"} + + logger.Info("msg", + slog.Any("s", s), + ) + + expected := []byte("\x1b[2m\x1b[37m[]\x1b[0m \x1b[42m\x1b[30m INFO \x1b[0m \x1b[32mmsg\x1b[0m\n\x1b[32mS\x1b[0m \x1b[35ms\x1b[0m: \x1b[34m2\x1b[0m \x1b[32m[\x1b[0m\x1b[32m]\x1b[0m\x1b[33ms\x1b[0m\x1b[33mt\x1b[0m\x1b[33mr\x1b[0m\x1b[33mi\x1b[0m\x1b[33mn\x1b[0m\x1b[33mg\x1b[0m\n \x1b[32m0\x1b[0m: apple\n \x1b[32m1\x1b[0m: ba na na\n\n") + + if !bytes.Equal(w.WrittenData, expected) { + t.Errorf("\nExpected:\n%s\nGot:\n%s\nExpected:\n%[1]q\nGot:\n%[2]q", expected, w.WrittenData) + } +} + +func test_SliceBig(t *testing.T, o *Options) { + w := &MockWriter{} + logger := slog.New(NewHandler(w, o)) + + s := make([]int, 0) + for i := 0; i < 11; i++ { + s = append(s, i*2) + } + + logger.Info("msg", + slog.Any("s", s), + ) + + expected := []byte("\x1b[2m\x1b[37m[]\x1b[0m \x1b[42m\x1b[30m INFO \x1b[0m \x1b[32mmsg\x1b[0m\n\x1b[32mS\x1b[0m \x1b[35ms\x1b[0m: \x1b[34m11\x1b[0m \x1b[32m[\x1b[0m\x1b[32m]\x1b[0m\x1b[33mi\x1b[0m\x1b[33mn\x1b[0m\x1b[33mt\x1b[0m\n \x1b[32m0\x1b[0m: 0\n \x1b[32m1\x1b[0m: 2\n \x1b[32m2\x1b[0m: 4\n \x1b[32m3\x1b[0m: 6\n \x1b[34m...\x1b[0m\x1b[32m]\x1b[0m\n\n") + + if !bytes.Equal(w.WrittenData, expected) { + t.Errorf("\nExpected:\n%s\nGot:\n%s\nExpected:\n%[1]q\nGot:\n%[2]q", expected, w.WrittenData) + } +} + +func test_Map(t *testing.T, o *Options) { + w := &MockWriter{} + logger := slog.New(NewHandler(w, o)) + + m := map[int]string{0: "a", 1: "b"} + mp := &m + + logger.Info("msg", + slog.Any("m", m), + slog.Any("mp", mp), + slog.Any("mpp", &mp), + ) + + expected := []byte("\x1b[2m\x1b[37m[]\x1b[0m \x1b[42m\x1b[30m INFO \x1b[0m \x1b[32mmsg\x1b[0m\n\x1b[32mM\x1b[0m \x1b[35mm\x1b[0m : \x1b[34m2\x1b[0m \x1b[33mm\x1b[0m\x1b[33ma\x1b[0m\x1b[33mp\x1b[0m\x1b[32m[\x1b[0m\x1b[33mi\x1b[0m\x1b[33mn\x1b[0m\x1b[33mt\x1b[0m\x1b[32m]\x1b[0m\x1b[33ms\x1b[0m\x1b[33mt\x1b[0m\x1b[33mr\x1b[0m\x1b[33mi\x1b[0m\x1b[33mn\x1b[0m\x1b[33mg\x1b[0m\n \x1b[32m0\x1b[0m: a\n \x1b[32m1\x1b[0m: b\n\x1b[32mM\x1b[0m \x1b[35mmp\x1b[0m : \x1b[34m2\x1b[0m \x1b[31m*\x1b[0m\x1b[33mm\x1b[0m\x1b[33ma\x1b[0m\x1b[33mp\x1b[0m\x1b[32m[\x1b[0m\x1b[33mi\x1b[0m\x1b[33mn\x1b[0m\x1b[33mt\x1b[0m\x1b[32m]\x1b[0m\x1b[33ms\x1b[0m\x1b[33mt\x1b[0m\x1b[33mr\x1b[0m\x1b[33mi\x1b[0m\x1b[33mn\x1b[0m\x1b[33mg\x1b[0m\n \x1b[32m0\x1b[0m: a\n \x1b[32m1\x1b[0m: b\n\x1b[32mM\x1b[0m \x1b[35mmpp\x1b[0m: \x1b[34m2\x1b[0m \x1b[31m*\x1b[0m\x1b[31m*\x1b[0m\x1b[33mm\x1b[0m\x1b[33ma\x1b[0m\x1b[33mp\x1b[0m\x1b[32m[\x1b[0m\x1b[33mi\x1b[0m\x1b[33mn\x1b[0m\x1b[33mt\x1b[0m\x1b[32m]\x1b[0m\x1b[33ms\x1b[0m\x1b[33mt\x1b[0m\x1b[33mr\x1b[0m\x1b[33mi\x1b[0m\x1b[33mn\x1b[0m\x1b[33mg\x1b[0m\n \x1b[32m0\x1b[0m: a\n \x1b[32m1\x1b[0m: b\n\n") + + if !bytes.Equal(w.WrittenData, expected) { + t.Errorf("\nExpected:\n%s\nGot:\n%s\nExpected:\n%[1]q\nGot:\n%[2]q", expected, w.WrittenData) + } +} + +func test_MapOfPointers(t *testing.T, o *Options) { + w := &MockWriter{} + logger := slog.New(NewHandler(w, o)) + + s := "a" + m := map[int]*string{0: &s, 1: &s} + + logger.Info("msg", + slog.Any("m", m), + ) + + expected := []byte("\x1b[2m\x1b[37m[]\x1b[0m \x1b[42m\x1b[30m INFO \x1b[0m \x1b[32mmsg\x1b[0m\n\x1b[32mM\x1b[0m \x1b[35mm\x1b[0m: \x1b[34m2\x1b[0m \x1b[33mm\x1b[0m\x1b[33ma\x1b[0m\x1b[33mp\x1b[0m\x1b[32m[\x1b[0m\x1b[33mi\x1b[0m\x1b[33mn\x1b[0m\x1b[33mt\x1b[0m\x1b[32m]\x1b[0m\x1b[31m*\x1b[0m\x1b[33ms\x1b[0m\x1b[33mt\x1b[0m\x1b[33mr\x1b[0m\x1b[33mi\x1b[0m\x1b[33mn\x1b[0m\x1b[33mg\x1b[0m\n \x1b[32m0\x1b[0m: a\n \x1b[32m1\x1b[0m: a\n\n") + + if !bytes.Equal(w.WrittenData, expected) { + t.Errorf("\nExpected:\n%s\nGot:\n%s\nExpected:\n%[1]q\nGot:\n%[2]q", expected, w.WrittenData) + } +} + +func test_Struct(t *testing.T, o *Options) { + w := &MockWriter{} + logger := slog.New(NewHandler(w, o)) + + type StructTest struct { + Slice []int + Map map[int]int + Struct struct{ B bool } + SliceP *[]int + MapP *map[int]int + StructP *struct{ B bool } + } + + s := &StructTest{ + Slice: []int{}, + Map: map[int]int{}, + Struct: struct{ B bool }{}, + SliceP: &[]int{}, + MapP: &map[int]int{}, + StructP: &struct{ B bool }{}, + } + + logger.Info("msg", + slog.Any("s", s), + ) + + expected := []byte("\x1b[2m\x1b[37m[]\x1b[0m \x1b[42m\x1b[30m INFO \x1b[0m \x1b[32mmsg\x1b[0m\n\x1b[33mS\x1b[0m \x1b[35ms\x1b[0m: \x1b[31m*\x1b[0m\x1b[33md\x1b[0m\x1b[33me\x1b[0m\x1b[33mv\x1b[0m\x1b[33ms\x1b[0m\x1b[33ml\x1b[0m\x1b[33mo\x1b[0m\x1b[33mg\x1b[0m\x1b[33m.\x1b[0m\x1b[33mS\x1b[0m\x1b[33mt\x1b[0m\x1b[33mr\x1b[0m\x1b[33mu\x1b[0m\x1b[33mc\x1b[0m\x1b[33mt\x1b[0m\x1b[33mT\x1b[0m\x1b[33me\x1b[0m\x1b[33ms\x1b[0m\x1b[33mt\x1b[0m\n \x1b[32mSlice\x1b[0m : \x1b[34m0\x1b[0m \x1b[32m[\x1b[0m\x1b[32m]\x1b[0m\x1b[33mi\x1b[0m\x1b[33mn\x1b[0m\x1b[33mt\x1b[0m\n \x1b[32mMap\x1b[0m : \x1b[34m0\x1b[0m \x1b[33mm\x1b[0m\x1b[33ma\x1b[0m\x1b[33mp\x1b[0m\x1b[32m[\x1b[0m\x1b[33mi\x1b[0m\x1b[33mn\x1b[0m\x1b[33mt\x1b[0m\x1b[32m]\x1b[0m\x1b[33mi\x1b[0m\x1b[33mn\x1b[0m\x1b[33mt\x1b[0m\n \x1b[32mStruct\x1b[0m : \x1b[33ms\x1b[0m\x1b[33mt\x1b[0m\x1b[33mr\x1b[0m\x1b[33mu\x1b[0m\x1b[33mc\x1b[0m\x1b[33mt\x1b[0m\x1b[33m \x1b[0m\x1b[33m{\x1b[0m\x1b[33m \x1b[0m\x1b[33mB\x1b[0m\x1b[33m \x1b[0m\x1b[33mb\x1b[0m\x1b[33mo\x1b[0m\x1b[33mo\x1b[0m\x1b[33ml\x1b[0m\x1b[33m \x1b[0m\x1b[33m}\x1b[0m\n \x1b[32mB\x1b[0m: false\n \x1b[32mSliceP\x1b[0m : \x1b[34m0\x1b[0m \x1b[31m*\x1b[0m\x1b[32m[\x1b[0m\x1b[32m]\x1b[0m\x1b[33mi\x1b[0m\x1b[33mn\x1b[0m\x1b[33mt\x1b[0m\n \x1b[32mMapP\x1b[0m : \x1b[34m0\x1b[0m \x1b[31m*\x1b[0m\x1b[33mm\x1b[0m\x1b[33ma\x1b[0m\x1b[33mp\x1b[0m\x1b[32m[\x1b[0m\x1b[33mi\x1b[0m\x1b[33mn\x1b[0m\x1b[33mt\x1b[0m\x1b[32m]\x1b[0m\x1b[33mi\x1b[0m\x1b[33mn\x1b[0m\x1b[33mt\x1b[0m\n \x1b[32mStructP\x1b[0m: \x1b[31m*\x1b[0m\x1b[33ms\x1b[0m\x1b[33mt\x1b[0m\x1b[33mr\x1b[0m\x1b[33mu\x1b[0m\x1b[33mc\x1b[0m\x1b[33mt\x1b[0m\x1b[33m \x1b[0m\x1b[33m{\x1b[0m\x1b[33m \x1b[0m\x1b[33mB\x1b[0m\x1b[33m \x1b[0m\x1b[33mb\x1b[0m\x1b[33mo\x1b[0m\x1b[33mo\x1b[0m\x1b[33ml\x1b[0m\x1b[33m \x1b[0m\x1b[33m}\x1b[0m\n \x1b[32mB\x1b[0m: false\n\n") + + if !bytes.Equal(w.WrittenData, expected) { + t.Errorf("\nExpected:\n%s\nGot:\n%s\nExpected:\n%[1]q\nGot:\n%[2]q", expected, w.WrittenData) + } +} + +func test_Group(t *testing.T, o *Options) { + w := &MockWriter{} + logger := slog.New(NewHandler(w, o)) + + logger.Info("msg", + slog.Any("1", "a"), + slog.Group("g", + slog.Any("2", "b"), + ), + ) + + expected := []byte("\x1b[2m\x1b[37m[]\x1b[0m \x1b[42m\x1b[30m INFO \x1b[0m \x1b[32mmsg\x1b[0m\n \x1b[35m1\x1b[0m: a\n\x1b[32mG\x1b[0m \x1b[35mg\x1b[0m: \x1b[32m============\x1b[0m\n \x1b[35m2\x1b[0m: b\n\n") + + if !bytes.Equal(w.WrittenData, expected) { + t.Errorf("\nExpected:\n%s\nGot:\n%s\nExpected:\n%[1]q\nGot:\n%[2]q", expected, w.WrittenData) + } +}