Skip to content

Commit

Permalink
os: make errors.Is work with ErrPermission et al.
Browse files Browse the repository at this point in the history
As proposed in Issue #29934, update errors produced by the os package to
work with errors.Is sentinel tests. For example,
errors.Is(err, os.ErrPermission) is equivalent to os.IsPermission(err)
with added unwrapping support.

Move the definition for os.ErrPermission and others into the syscall
package. Add an Is method to syscall.Errno and others. Add an Unwrap
method to os.PathError and others.

Updates #30322
Updates #29934

Change-Id: I95727d26c18a5354c720de316dff0bffc04dd926
Reviewed-on: https://go-review.googlesource.com/c/go/+/163058
Reviewed-by: Marcel van Lohuizen <[email protected]>
  • Loading branch information
neild committed Mar 20, 2019
1 parent af7b757 commit a919b76
Show file tree
Hide file tree
Showing 14 changed files with 201 additions and 115 deletions.
4 changes: 2 additions & 2 deletions src/fmt/errors_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,8 +157,8 @@ func TestErrorFormatter(t *testing.T) {
want: "fallback:" +
"\n somefile.go:123" +
"\n - file does not exist:" +
"\n os.init" +
"\n .+/os/error.go:\\d\\d",
"\n .*" +
"\n .+.go:\\d+",
regexp: true,
}, {
err: &wrapped{"outer",
Expand Down
5 changes: 3 additions & 2 deletions src/go/build/deps_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -146,8 +146,9 @@ var pkgDeps = map[string][]string{
// End of linear dependency definitions.

// Operating system access.
"syscall": {"L0", "internal/race", "internal/syscall/windows/sysdll", "syscall/js", "unicode/utf16"},
"syscall": {"L0", "internal/oserror", "internal/race", "internal/syscall/windows/sysdll", "syscall/js", "unicode/utf16"},
"syscall/js": {"L0"},
"internal/oserror": {"L0"},
"internal/syscall/unix": {"L0", "syscall"},
"internal/syscall/windows": {"L0", "syscall", "internal/syscall/windows/sysdll"},
"internal/syscall/windows/registry": {"L0", "syscall", "internal/syscall/windows/sysdll", "unicode/utf16"},
Expand All @@ -167,7 +168,7 @@ var pkgDeps = map[string][]string{

"internal/poll": {"L0", "internal/race", "syscall", "time", "unicode/utf16", "unicode/utf8", "internal/syscall/windows"},
"internal/testlog": {"L0"},
"os": {"L1", "os", "syscall", "time", "internal/poll", "internal/syscall/windows", "internal/syscall/unix", "internal/testlog"},
"os": {"L1", "os", "syscall", "time", "internal/oserror", "internal/poll", "internal/syscall/windows", "internal/syscall/unix", "internal/testlog"},
"path/filepath": {"L2", "os", "syscall", "internal/syscall/windows"},
"io/ioutil": {"L2", "os", "path/filepath", "time"},
"os/exec": {"L2", "os", "context", "path/filepath", "syscall"},
Expand Down
20 changes: 20 additions & 0 deletions src/internal/oserror/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Copyright 2019 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Package oserror defines errors values used in the os package.
//
// These types are defined here to permit the syscall package to reference them.
package oserror

import "errors"

var (
ErrInvalid = errors.New("invalid argument")
ErrPermission = errors.New("permission denied")
ErrExist = errors.New("file already exists")
ErrNotExist = errors.New("file does not exist")
ErrClosed = errors.New("file already closed")
ErrTemporary = errors.New("temporary error")
ErrTimeout = errors.New("deadline exceeded")
)
49 changes: 39 additions & 10 deletions src/os/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,35 @@
package os

import (
"errors"
"internal/oserror"
"internal/poll"
)

// Portable analogs of some common system call errors.
//
// Errors returned from this package may be tested against these errors
// with errors.Is.
var (
ErrInvalid = errors.New("invalid argument") // methods on File will return this error when the receiver is nil
ErrPermission = errors.New("permission denied")
ErrExist = errors.New("file already exists")
ErrNotExist = errors.New("file does not exist")
ErrClosed = errors.New("file already closed")
ErrNoDeadline = poll.ErrNoDeadline
// ErrInvalid indicates an invalid argument.
// Methods on File will return this error when the receiver is nil.
ErrInvalid = errInvalid() // "invalid argument"

ErrPermission = errPermission() // "permission denied"
ErrExist = errExist() // "file already exists"
ErrNotExist = errNotExist() // "file does not exist"
ErrClosed = errClosed() // "file already closed"
ErrTimeout = errTimeout() // "deadline exceeded"
ErrNoDeadline = errNoDeadline() // "file type does not support deadline"
)

func errInvalid() error { return oserror.ErrInvalid }
func errPermission() error { return oserror.ErrPermission }
func errExist() error { return oserror.ErrExist }
func errNotExist() error { return oserror.ErrNotExist }
func errClosed() error { return oserror.ErrClosed }
func errTimeout() error { return oserror.ErrTimeout }
func errNoDeadline() error { return poll.ErrNoDeadline }

type timeout interface {
Timeout() bool
}
Expand Down Expand Up @@ -48,6 +63,8 @@ type SyscallError struct {

func (e *SyscallError) Error() string { return e.Syscall + ": " + e.Err.Error() }

func (e *SyscallError) Unwrap() error { return e.Err }

// Timeout reports whether this error represents a timeout.
func (e *SyscallError) Timeout() bool {
t, ok := e.Err.(timeout)
Expand All @@ -68,21 +85,21 @@ func NewSyscallError(syscall string, err error) error {
// that a file or directory already exists. It is satisfied by ErrExist as
// well as some syscall errors.
func IsExist(err error) bool {
return isExist(err)
return underlyingErrorIs(err, ErrExist)
}

// IsNotExist returns a boolean indicating whether the error is known to
// report that a file or directory does not exist. It is satisfied by
// ErrNotExist as well as some syscall errors.
func IsNotExist(err error) bool {
return isNotExist(err)
return underlyingErrorIs(err, ErrNotExist)
}

// IsPermission returns a boolean indicating whether the error is known to
// report that permission is denied. It is satisfied by ErrPermission as well
// as some syscall errors.
func IsPermission(err error) bool {
return isPermission(err)
return underlyingErrorIs(err, ErrPermission)
}

// IsTimeout returns a boolean indicating whether the error is known
Expand All @@ -92,6 +109,18 @@ func IsTimeout(err error) bool {
return ok && terr.Timeout()
}

func underlyingErrorIs(err, target error) bool {
// Note that this function is not errors.Is:
// underlyingError only unwraps the specific error-wrapping types
// that it historically did, not all errors.Wrapper implementations.
err = underlyingError(err)
if err == target {
return true
}
e, ok := err.(interface{ Is(error) bool })
return ok && e.Is(target)
}

// underlyingError returns the underlying error for known os error types.
func underlyingError(err error) error {
switch err := err.(type) {
Expand Down
44 changes: 0 additions & 44 deletions src/os/error_plan9.go

This file was deleted.

20 changes: 16 additions & 4 deletions src/os/error_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ func TestErrIsExist(t *testing.T) {
t.Fatal("Open should have failed")
return
}
if s := checkErrorPredicate("os.IsExist", os.IsExist, err); s != "" {
if s := checkErrorPredicate("os.IsExist", os.IsExist, err, os.ErrExist); s != "" {
t.Fatal(s)
return
}
Expand All @@ -39,15 +39,15 @@ func testErrNotExist(name string) string {
f.Close()
return "Open should have failed"
}
if s := checkErrorPredicate("os.IsNotExist", os.IsNotExist, err); s != "" {
if s := checkErrorPredicate("os.IsNotExist", os.IsNotExist, err, os.ErrNotExist); s != "" {
return s
}

err = os.Chdir(name)
if err == nil {
return "Chdir should have failed"
}
if s := checkErrorPredicate("os.IsNotExist", os.IsNotExist, err); s != "" {
if s := checkErrorPredicate("os.IsNotExist", os.IsNotExist, err, os.ErrNotExist); s != "" {
return s
}
return ""
Expand All @@ -74,10 +74,13 @@ func TestErrIsNotExist(t *testing.T) {
}
}

func checkErrorPredicate(predName string, pred func(error) bool, err error) string {
func checkErrorPredicate(predName string, pred func(error) bool, err, target error) string {
if !pred(err) {
return fmt.Sprintf("%s does not work as expected for %#v", predName, err)
}
if !errors.Is(err, target) {
return fmt.Sprintf("errors.Is(%#v, %#v) = false, want true", err, target)
}
return ""
}

Expand Down Expand Up @@ -108,9 +111,15 @@ func TestIsExist(t *testing.T) {
if is := os.IsExist(tt.err); is != tt.is {
t.Errorf("os.IsExist(%T %v) = %v, want %v", tt.err, tt.err, is, tt.is)
}
if is := errors.Is(tt.err, os.ErrExist); is != tt.is {
t.Errorf("errors.Is(%T %v, os.ErrExist) = %v, want %v", tt.err, tt.err, is, tt.is)
}
if isnot := os.IsNotExist(tt.err); isnot != tt.isnot {
t.Errorf("os.IsNotExist(%T %v) = %v, want %v", tt.err, tt.err, isnot, tt.isnot)
}
if isnot := errors.Is(tt.err, os.ErrNotExist); isnot != tt.isnot {
t.Errorf("errors.Is(%T %v, os.ErrNotExist) = %v, want %v", tt.err, tt.err, isnot, tt.isnot)
}
}
}

Expand All @@ -130,6 +139,9 @@ func TestIsPermission(t *testing.T) {
if got := os.IsPermission(tt.err); got != tt.want {
t.Errorf("os.IsPermission(%#v) = %v; want %v", tt.err, got, tt.want)
}
if got := errors.Is(tt.err, os.ErrPermission); got != tt.want {
t.Errorf("errors.Is(%#v, os.ErrPermission) = %v; want %v", tt.err, got, tt.want)
}
}
}

Expand Down
24 changes: 0 additions & 24 deletions src/os/error_unix.go

This file was deleted.

28 changes: 0 additions & 28 deletions src/os/error_windows.go

This file was deleted.

4 changes: 4 additions & 0 deletions src/os/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,10 @@ func (e *LinkError) Error() string {
return e.Op + " " + e.Old + " " + e.New + ": " + e.Err.Error()
}

func (e *LinkError) Unwrap() error {
return e.Err
}

// Read reads up to len(b) bytes from the File.
// It returns the number of bytes read and any error encountered.
// At end of file, Read returns 0, io.EOF.
Expand Down
17 changes: 17 additions & 0 deletions src/syscall/syscall_js.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
package syscall

import (
"internal/oserror"
"sync"
"unsafe"
)
Expand Down Expand Up @@ -55,6 +56,22 @@ func (e Errno) Error() string {
return "errno " + itoa(int(e))
}

func (e Errno) Is(target error) bool {
switch target {
case oserror.ErrTemporary:
return e.Temporary()
case oserror.ErrTimeout:
return e.Timeout()
case oserror.ErrPermission:
return e == EACCES || e == EPERM
case oserror.ErrExist:
return e == EEXIST || e == ENOTEMPTY
case oserror.ErrNotExist:
return e == ENOENT
}
return false
}

func (e Errno) Temporary() bool {
return e == EINTR || e == EMFILE || e.Timeout()
}
Expand Down
17 changes: 17 additions & 0 deletions src/syscall/syscall_nacl.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
package syscall

import (
"internal/oserror"
"sync"
"unsafe"
)
Expand Down Expand Up @@ -62,6 +63,22 @@ func (e Errno) Error() string {
return "errno " + itoa(int(e))
}

func (e Errno) Is(target error) bool {
switch target {
case oserror.ErrTemporary:
return e.Temporary()
case oserror.ErrTimeout:
return e.Timeout()
case oserror.ErrPermission:
return e == EACCES || e == EPERM
case oserror.ErrExist:
return e == EEXIST || e == ENOTEMPTY
case oserror.ErrNotExist:
return e == ENOENT
}
return false
}

func (e Errno) Temporary() bool {
return e == EINTR || e == EMFILE || e.Timeout()
}
Expand Down
Loading

0 comments on commit a919b76

Please sign in to comment.