Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Print the stack trace when formatting with %+v #28

Merged
merged 2 commits into from
May 9, 2017
Merged

Print the stack trace when formatting with %+v #28

merged 2 commits into from
May 9, 2017

Conversation

mkmik
Copy link
Contributor

@mkmik mkmik commented Jan 19, 2017

Discovered in:
http://dave.cheney.net/2016/06/12/stack-traces-and-the-errors-package

Code based on:
https://github.com/pkg/errors

With a minor but hackish change to still allow %#v to print the structure fields, in order to not break the principle of least surprise. Let me know if there is a better way to strip a type of an interface or invoke the default %#v behaviour on a type that implements fmt.Formatter.


NOTE: this is the continuation of #26. I no longer work for cesanta and thus cannot address comments in the original PR.

#26 received the green light from @mjs, and had an open question by @howbazaar, whom I replied to.

// helper for Format
type unformatter Err

func (unformatter) Format() { /* break the fmt.Formatter interface */ }
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this take (fmt.State, rune)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

well, in that case it would fulfill the fmt.Formatter interface.
By adding a method with an incompatible type signature we're breaking the interface.

This allows us to do:

fmt.Fprintf(s, "%#v", (*unformatter)(e))

otherwise fmt.Fprintf will do a runtime type assertion e.(fmt.Formatter), which will succeed and cause infinite recursion here.

@mjs, do you know of a better way to preserve the default rendering that the object would have had with %#v?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no need to define a method - the unformatter type has no methods, because a newly defined named type in Go has no methods (other than those from embedded types).
I'd suggest defining the unformatter type inline inside Err.Format.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes indeed! thanks!
I believe I confused it with the embedding rules.

@mkmik
Copy link
Contributor Author

mkmik commented Apr 21, 2017

@howbazaar, @mjs friendly ping

@wallyworld
Copy link
Member

$$merge$$

@jujubot
Copy link
Contributor

jujubot commented May 9, 2017

Status: merge request accepted. Url: http://juju-ci.vapour.ws:8080/job/github-merge-juju-errors

@jujubot
Copy link
Contributor

jujubot commented May 9, 2017

Build failed: Tests failed
build url: http://juju-ci.vapour.ws:8080/job/github-merge-juju-errors/8

Copy link
Contributor

@rogpeppe rogpeppe left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks reasonable, but this really needs some tests, and modulo a few comments and suggestions.

// helper for Format
type unformatter Err

func (unformatter) Format() { /* break the fmt.Formatter interface */ }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no need to define a method - the unformatter type has no methods, because a newly defined named type in Go has no methods (other than those from embedded types).
I'd suggest defining the unformatter type inline inside Err.Format.

fallthrough
case 's':
fmt.Fprintf(s, "%s", e.Error())
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add a default case so that we get some indication when someone uses the wrong verb.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also added support for %T

}
fallthrough
case 's':
fmt.Fprintf(s, "%s", e.Error())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s.Write([]byte(e.Error()))
?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

while we're at it, let's use io.WriteString

case s.Flag('#'):
// avoid infinite recursion by wrapping e into a type
// that doesn't implement Formatter.
fmt.Fprintf(s, "%#v", (*unformatter)(e))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't going to produce quite what you want, I think - it'll show the "unformatter" type name, which will be quite unexpected.

It depends how useful we think the default %#v output is - personally, I think I'd write errors.Details(e) here, which means that you'd get reasonable default output when %#v is used (for example when gopkg.in/check.v1 prints a value).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no strong opinions here; my expectations for %#v was to show the actual thing without any smart reformatting, i.e. as the doc says:

%#v	a Go-syntax representation of the value

So currently, with gopkg.in/check.v1 you see the guts of juju/errors:

	err := fmt.Errorf("foo")
	c.Check(err, Equals, 42)
	err = errors.Trace(err)
	c.Check(err, Equals, 42)

... go test:

foo_test.go:21:
    c.Check(err, Equals, 42)
... obtained *errors.errorString = &errors.errorString{s:"foo"} ("foo")
... expected int = 42

foo_test.go:23:
    c.Check(err, Equals, 42)
... obtained *errors.Err = &errors.Err{message:"", cause:(*errors.errorString)(0xc42000ef60), previous:(*errors.errorString)(0xc42000ef60), file:"pippo/foo_test.go", line:22} ("foo")
... expected int = 42

With this PR, the latter will become:

&errors.unformatter{message:"", cause:(*errors.errorString)(0xc42000ef60), previous:(*errors.errorString)(0xc42000ef60), file:"pippo/foo_test.go", line:22}

IMHO it's a bit ugly but at least it's not hiding anything.

@rogpeppe: Still prefer errors.Details(e) ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rogpeppe: or what about this: mangle the output so that it outputs &errors.Err{ exactly as before

@mkmik
Copy link
Contributor Author

mkmik commented May 9, 2017

Looks reasonable, but this really needs some tests ...

Yeah, makes sense. I'll add some test

@jujubot jujubot merged commit 8234c82 into juju:master May 9, 2017
@mkmik
Copy link
Contributor Author

mkmik commented May 9, 2017

jujubot trigger happy :-)
I'll add tests in another PR

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants