Skip to content

Commit

Permalink
funcr: Handle nil Stringer, Marshaler, error
Browse files Browse the repository at this point in the history
Produce a useful error rather than panic when we can be sure that the
panic was a nil-ptr.  Otherwise pass the panic on.
  • Loading branch information
thockin committed Jan 20, 2022
1 parent eb02c45 commit deee1ed
Show file tree
Hide file tree
Showing 2 changed files with 141 additions and 11 deletions.
56 changes: 53 additions & 3 deletions funcr/funcr.go
Original file line number Diff line number Diff line change
Expand Up @@ -351,15 +351,15 @@ func (f Formatter) prettyWithFlags(value interface{}, flags uint32, depth int) s
if v, ok := value.(logr.Marshaler); ok {
// Replace the value with what the type wants to get logged.
// That then gets handled below via reflection.
value = v.MarshalLog()
value = invokeMarshaler(v)
}

// Handle types that want to format themselves.
switch v := value.(type) {
case fmt.Stringer:
value = v.String()
value = invokeStringer(v)
case error:
value = v.Error()
value = invokeError(v)
}

// Handling the most common types without reflect is a small perf win.
Expand Down Expand Up @@ -596,6 +596,56 @@ func isEmpty(v reflect.Value) bool {
return false
}

func invokeMarshaler(m logr.Marshaler) (ret interface{}) {
defer func() {
if err := recover(); err != nil {
if isNil(m) {
ret = "<nil-logr-marshaler>"
} else {
panic(err)
}
}
}()
return m.MarshalLog()
}

func invokeStringer(s fmt.Stringer) (ret string) {
defer func() {
if err := recover(); err != nil {
if isNil(s) {
ret = "<nil-fmt-stringer>"
} else {
panic(err)
}
}
}()
return s.String()
}

func invokeError(e error) (ret string) {
defer func() {
if isNil(e) {
if err := recover(); err != nil {
ret = "<nil-error>"
} else {
panic(err)
}
}
}()
return e.Error()
}

func isNil(i interface{}) bool {
if i == nil {
return true
}
switch reflect.TypeOf(i).Kind() {
case reflect.Ptr, reflect.Map, reflect.Array, reflect.Chan, reflect.Slice:
return reflect.ValueOf(i).IsNil()
}
return false
}

// Caller represents the original call site for a log line, after considering
// logr.Logger.WithCallDepth and logr.Logger.WithCallStackHelper. The File and
// Line fields will always be provided, while the Func field is optional.
Expand Down
96 changes: 88 additions & 8 deletions funcr/funcr_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ func (p pointErr) MarshalText() ([]byte, error) {
}

// Logging this should result in the MarshalLog() value.
type Tmarshaler string
type Tmarshaler struct{ val string }

func (t Tmarshaler) MarshalLog() interface{} {
return struct{ Inner string }{"I am a logr.Marshaler"}
Expand All @@ -66,8 +66,15 @@ func (t Tmarshaler) Error() string {
return "Error(): you should not see this"
}

// Logging this should result in a panic.
type Tmarshalerpanic struct{ val string }

func (t Tmarshalerpanic) MarshalLog() interface{} {
panic("Tmarshalerpanic")
}

// Logging this should result in the String() value.
type Tstringer string
type Tstringer struct{ val string }

func (t Tstringer) String() string {
return "I am a fmt.Stringer"
Expand All @@ -77,6 +84,27 @@ func (t Tstringer) Error() string {
return "Error(): you should not see this"
}

// Logging this should result in a panic.
type Tstringerpanic struct{ val string }

func (t Tstringerpanic) String() string {
panic("Tstringerpanic")
}

// Logging this should result in the Error() value.
type Terror struct{ val string }

func (t Terror) Error() string {
return "I am an error"
}

// Logging this should result in a panic.
type Terrorpanic struct{ val string }

func (t Terrorpanic) Error() string {
panic("Terrorpanic")
}

type TjsontagsString struct {
String1 string `json:"string1"` // renamed
String2 string `json:"-"` // ignored
Expand Down Expand Up @@ -209,8 +237,9 @@ func TestPretty(t *testing.T) {
}

cases := []struct {
val interface{}
exp string // used in cases where JSON can't handle it
val interface{}
exp string // used in cases where JSON can't handle it
pmsg string // used to test panic cases
}{
{val: "strval"},
{val: "strval\nwith\t\"escapes\""},
Expand Down Expand Up @@ -351,16 +380,52 @@ func TestPretty(t *testing.T) {
},
},
{
val: Tmarshaler("foobar"),
val: Tmarshaler{"foobar"},
exp: `{"Inner":"I am a logr.Marshaler"}`,
},
{
val: &Tmarshaler{"foobar"},
exp: `{"Inner":"I am a logr.Marshaler"}`,
},
{
val: Tstringer("foobar"),
val: (*Tmarshaler)(nil),
exp: `"<nil-logr-marshaler>"`,
},
{
val: Tmarshalerpanic{"foobar"},
pmsg: "Tmarshalerpanic",
},
{
val: Tstringer{"foobar"},
exp: `"I am a fmt.Stringer"`,
},
{
val: &Tstringer{"foobar"},
exp: `"I am a fmt.Stringer"`,
},
{
val: fmt.Errorf("error"),
exp: `"error"`,
val: (*Tstringer)(nil),
exp: `"<nil-fmt-stringer>"`,
},
{
val: Tstringerpanic{"foobar"},
pmsg: "Tstringerpanic",
},
{
val: Terror{"foobar"},
exp: `"I am an error"`,
},
{
val: &Terror{"foobar"},
exp: `"I am an error"`,
},
{
val: (*Terror)(nil),
exp: `"<nil-error>"`,
},
{
val: Terrorpanic{"foobar"},
pmsg: "Terrorpanic",
},
{
val: TjsontagsString{
Expand Down Expand Up @@ -514,6 +579,21 @@ func TestPretty(t *testing.T) {

f := NewFormatter(Options{})
for i, tc := range cases {
// Handle cases that are supposed to panic.
if tc.pmsg != "" {
func() {
defer func() {
r := recover()
if r == nil {
t.Errorf("[%d]: expected panic(%q)", i, tc.pmsg)
} else if fmt.Sprintf("%v", r) != tc.pmsg {
t.Errorf("[%d]: expected panic(%q), got %q", i, tc.pmsg, r)
}
}()
_ = f.pretty(tc.val)
}()
continue
}
ours := f.pretty(tc.val)
want := ""
if tc.exp != "" {
Expand Down

0 comments on commit deee1ed

Please sign in to comment.