New helper methods for generating readable loggable strings (#20911)

This commit is contained in:
Nick Cabatoff
2023-06-01 09:12:16 -04:00
committed by GitHub
parent e32cf520f4
commit 65157a6d3f
2 changed files with 126 additions and 0 deletions

View File

@@ -0,0 +1,81 @@
package testhelpers
import (
"crypto/sha256"
"fmt"
"reflect"
"github.com/mitchellh/go-testing-interface"
"github.com/mitchellh/mapstructure"
)
// ToMap renders an input value of any type as a map. This is intended for
// logging human-readable data dumps in test logs, so it uses the `json`
// tags on struct fields: this makes it easy to exclude `"-"` values that
// are typically not interesting, respect omitempty, etc.
//
// We also replace any []byte fields with a hash of their value.
// This is usually sufficient for test log purposes, and is a lot more readable
// than a big array of individual byte values like Go would normally stringify a
// byte slice.
func ToMap(in any) (map[string]any, error) {
temp := make(map[string]any)
cfg := &mapstructure.DecoderConfig{
TagName: "json",
IgnoreUntaggedFields: true,
Result: &temp,
}
md, err := mapstructure.NewDecoder(cfg)
if err != nil {
return nil, err
}
err = md.Decode(in)
if err != nil {
return nil, err
}
// mapstructure doesn't call the DecodeHook for each field when doing
// struct->map conversions, but it does for map->map, so call it a second
// time to convert each []byte field.
out := make(map[string]any)
md2, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
Result: &out,
DecodeHook: func(from reflect.Type, to reflect.Type, data interface{}) (interface{}, error) {
if from.Kind() != reflect.Slice || from.Elem().Kind() != reflect.Uint8 {
return data, nil
}
b := data.([]byte)
return fmt.Sprintf("%x", sha256.Sum256(b)), nil
},
})
if err != nil {
return nil, err
}
err = md2.Decode(temp)
if err != nil {
return nil, err
}
return out, nil
}
// ToString renders its input using ToMap, and returns a string containing the
// result or an error if that fails.
func ToString(in any) string {
m, err := ToMap(in)
if err != nil {
return err.Error()
}
return fmt.Sprintf("%v", m)
}
// StringOrDie renders its input using ToMap, and returns a string containing the
// result. If rendering yields an error, calls t.Fatal.
func StringOrDie(t testing.T, in any) string {
t.Helper()
m, err := ToMap(in)
if err != nil {
t.Fatal(err)
}
return fmt.Sprintf("%v", m)
}

View File

@@ -0,0 +1,45 @@
package testhelpers
import (
"fmt"
"reflect"
"testing"
)
func TestToMap(t *testing.T) {
type s struct {
A string `json:"a"`
B []byte `json:"b"`
C map[string]string `json:"c"`
D string `json:"-"`
}
type args struct {
in s
}
tests := []struct {
name string
args args
want string
wantErr bool
}{
{
name: "basic",
args: args{s{A: "a", B: []byte("bytes"), C: map[string]string{"k": "v"}, D: "d"}},
want: "map[a:a b:277089d91c0bdf4f2e6862ba7e4a07605119431f5d13f726dd352b06f1b206a9 c:map[k:v]]",
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
m, err := ToMap(&tt.args.in)
if (err != nil) != tt.wantErr {
t.Errorf("ToMap() error = %v, wantErr %v", err, tt.wantErr)
return
}
got := fmt.Sprintf("%s", m)
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("ToMap() got = %v, want %v", got, tt.want)
}
})
}
}