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

testing/fstest: should use errors.Is instead of equality #47062

Closed
zepatrik opened this issue Jul 6, 2021 · 15 comments
Closed

testing/fstest: should use errors.Is instead of equality #47062

zepatrik opened this issue Jul 6, 2021 · 15 comments
Labels
FrozenDueToAge NeedsDecision Feedback is required from experts, contributors, and/or the community before a change can be made.
Milestone

Comments

@zepatrik
Copy link

zepatrik commented Jul 6, 2021

What version of Go are you using (go version)?

$ go version
go version go1.16.5 linux/amd64

Does this issue reproduce with the latest release?

Yes

What did you do?

I wrote an fs.FS implementation that wraps errors with a stacktrace. Therefore I don't return io.EOF but errors.WithStack(io.EOF).
fstest.TestFS() fails at this line:

if len(list2) > 0 || err != io.EOF {

because it uses != instead of errors.Is().

I think all error checks in that test should instead use errors.Is() to check whether the right error is returned.

What did you expect to see?

No test failure.

What did you see instead?

Test failure.

@zepatrik
Copy link
Author

zepatrik commented Jul 6, 2021

Btw, I can contribute the fix and possibly also the "merge" filesystem from the linked PR if desired. It merges any number and type of file systems into one overarching.

@tmthrgd
Copy link
Contributor

tmthrgd commented Jul 6, 2021

io.EOF is considered special and should not normally be wrapped. The documentation mentions only Read, but I believe it applies anywhere: https://pkg.go.dev/io#pkg-variables

@peterbourgon
Copy link

Anyone know why io.EOF gets to be an exception to the advice in package errors about checking error identity?

@peterbourgon
Copy link

wraps errors with a stacktrace

There is no reason to do this. Callers who want a stack trace when they receive an error can easily generate one themselves. Callers who don't want a stack trace should not be required to pay the costs of generating one.

@seankhliao
Copy link
Member

related #39155

@neild
Copy link
Contributor

neild commented Jul 6, 2021

Anyone know why io.EOF gets to be an exception to the advice in package errors about checking error identity?

Because the io.Reader interface was well-defined and widely used (to put it mildly) by the time error wrapping was introduced.

Also, checks for io.EOF are likely to occur in a performance-critical hot path, which might have been sufficient reason to require an unwrapped sentinel anyway. Or possibly not; the existing definition of io.Reader made the question moot.

@peterbourgon
Copy link

Because the io.Reader interface was well-defined and widely used (to put it mildly) by the time error wrapping was introduced.

Is io.Reader the only widely-used interface that specifies a sentinel error as part of its API? Even if so, that's orthogonal to the way its caller checks for error identity, isn't it? And, concretely, fstest.TestFS() was (presumably) written post-1.13, so why isn't it using errors.Is(err, io.EOF)?

@neild
Copy link
Contributor

neild commented Jul 7, 2021

An io.Reader is specified as returning io.EOF at end-of-file, not an error wrapping io.EOF. The fstest package is testing for a conformant io.Reader, so it checks for an io.EOF. It reporting an error in this circumstance is an example of it functioning as intended: It has correctly detected an incorrect implementation.

I'm not certain if there are other interfaces in the standard library specified as returning a sentinel error. If there are, the rationale for Read returning an unwrapped io.EOF would apply to them as well.

As @seankhliao pointed out, there is more discussion of io.EOF and error wrapping in #39155.

@peterbourgon
Copy link

An io.Reader is specified as returning io.EOF at end-of-file, not an error wrapping io.EOF.

The linked discussion was informative, thank you.

I parse

EOF is the error returned by Read

as entirely compatible with wrapping + errors.Is; if that's not the intent, it should probably be made more explicit? And it's not just the Read method of an io.Reader that's bound by this constraint, but apparently the ReadDir method of a fs.ReadDirFile, too. Others?

@zepatrik
Copy link
Author

zepatrik commented Jul 7, 2021

OK, the discussion around EOF is interesting. Thanks for the explanation around that. My key takeaway is:

EOF is not an error, it is expected behavior

which in turn leaves me wonder why it is of type error in the first place, but that was a bad design decision made way back I assume.

There is no reason to do this. Callers who want a stack trace when they receive an error can easily generate one themselves. Callers who don't want a stack trace should not be required to pay the costs of generating one.

I am not sure I understand that. When I get the error at a point where I want to print/debug/... it, I don't have the program counter of where the error originated from? How can I generate a stack trace at that point? Can you maybe please give me some pointers on how that would be done? I would like to see the same output as if the code would have panicked instead of returning an error. The only way I know of how of how that is achievable is using runtime.Callers(), but that has to be called where the error originally was generated. Or can the same be achieved with tracing enabled somehow?

@peterbourgon
Copy link

Wildly off-topic and singular comment about errors and stack traces

I would like to see the same output as if the code would have panicked instead of returning an error.

You're treating errors as if they were exceptions, deferring handling to some top level catch-all, I'm guessing. But errors aren't exceptions. Whatever first receives an error should handle it, and if handling means returning it further up the stack, it should always be annotated first. This way, provenance is clear.

v, err := process(n)
if err != nil {
    // return nil, err // no
    return nil, fmt.Errorf("processing %d: %w", n, err) // yes
}

@mdlayher mdlayher changed the title fstest checks error equality instead of using errors.Is testing/fstest: should use errors.Is instead of equality Jul 7, 2021
@mknyszek mknyszek added the NeedsDecision Feedback is required from experts, contributors, and/or the community before a change can be made. label Jul 7, 2021
@mknyszek mknyszek added this to the Backlog milestone Jul 7, 2021
@mknyszek
Copy link
Contributor

mknyszek commented Jul 7, 2021

Based on the conversation, it doesn't look like we're going to be able to make this change because of io.Reader's contract. There's too much code out there that relies on == io.EOF, so I'm inclined to close this issue. @zepatrik Please comment if you feel it should be reopened.

@mknyszek mknyszek closed this as completed Jul 7, 2021
@neild
Copy link
Contributor

neild commented Jul 7, 2021

The specific case in fstest is actually not io.Reader, but fs.ReadDirFile. The io package documentation is fairly clear that Read must return an io.EOF without wrapping ("Read must return EOF itself, not an error wrapping EOF..."), but the fs package doesn't mention wrapping.

The requirements for fs.ReadDirFile could probably stand to be documented more clearly.

@mknyszek
Copy link
Contributor

mknyszek commented Jul 7, 2021

@neild Filed #47086 to track that.

@gopherbot
Copy link

Change https://golang.org/cl/333149 mentions this issue: io/fs: document requirement that ReadDir return an unwrapped io.EOF

gopherbot pushed a commit that referenced this issue May 17, 2022
This requirement ensures that ReadDir implementations are as compatible
as possible with "*os.File".ReadDir.

The testing/fstest package already tests for equality to io.EOF.

Updates #47062.
Fixes #47086.

Change-Id: I54f911a34e507a3db0abc4da55a19b7a50b35041
Reviewed-on: https://go-review.googlesource.com/c/go/+/333149
Reviewed-by: Michael Knyszek <mknyszek@google.com>
@golang golang locked and limited conversation to collaborators Jul 7, 2022
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
FrozenDueToAge NeedsDecision Feedback is required from experts, contributors, and/or the community before a change can be made.
Projects
None yet
Development

No branches or pull requests

7 participants