-
Notifications
You must be signed in to change notification settings - Fork 164
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
Implement custom error handling to Storer interface #590
Changes from all commits
1358c73
bb43faf
c52f22a
5fa4311
8efff56
e6dbc8d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
package main | ||
|
||
import "fmt" | ||
|
||
type ErrorCode int | ||
|
||
type Error struct { | ||
Code ErrorCode | ||
Message string | ||
} | ||
|
||
const ( | ||
NegativeValue ErrorCode = iota + 1001 | ||
IDNotFound | ||
Unknown = 1999 | ||
) | ||
|
||
func (e *Error) Error() string { | ||
return fmt.Sprintf("[%d] %s", e.Code, e.Message) | ||
} | ||
|
||
// NewError creates a new error with the given enum | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nitpick: Godoc comments should end with a full-stop, as they appear as sentences in the documentation (https://blog.golang.org/godoc-documenting-go-code). another nitpick: I'd say "given error code" instead of "given enum" - seems a little clearer to me. |
||
func NewError(code ErrorCode) error { | ||
switch code { | ||
case NegativeValue: | ||
return &Error{code, "Puppy value must be greater than 0"} | ||
case IDNotFound: | ||
return &Error{code, "Nonexistent Puppy ID"} | ||
default: | ||
return &Error{Unknown, "Unknown error"} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This throws away the error code passed in, which may be useful when trying to debug where a strange error is coming from. Perhaps set the string as |
||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
package main | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
func TestUnknownError(t *testing.T) { | ||
a := assert.New(t) | ||
|
||
error := NewError(123) | ||
a.Equal(error, NewError(Unknown)) | ||
} | ||
|
||
func TestError(t *testing.T) { | ||
a := assert.New(t) | ||
|
||
error := Error{1, "message"} | ||
formattedError := error.Error() | ||
a.Equal(formattedError, "[1] message") | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't see that you have 100% test coverage with this commit. There are a couple of branches in I will share the magic in a subsequent comment where I have noticed your tests failing... |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
package main | ||
|
||
import ( | ||
"fmt" | ||
"io" | ||
"os" | ||
) | ||
|
||
var out io.Writer = os.Stdout | ||
|
||
func main() { | ||
fmt.Fprint(out, "hello") | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
package main | ||
|
||
import ( | ||
"bytes" | ||
"testing" | ||
) | ||
|
||
func TestMainOutput(t *testing.T) { | ||
var buf bytes.Buffer | ||
out = &buf | ||
|
||
main() | ||
|
||
expected := "hello" | ||
|
||
got := buf.String() | ||
|
||
if expected != got { | ||
t.Errorf("\nExpected: %s\nGot: %s", expected, got) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
package main | ||
|
||
// NewMapStore returns a pointer to a new instance of the MapStore struct which implements the Storer interface. | ||
func NewMapStore() Storer { | ||
return &MapStore{ | ||
uuid: 0, | ||
store: map[int]Puppy{}, | ||
} | ||
} | ||
|
||
// Create increments the uuid and adds the provided Puppy struct to the store with this identifier. | ||
func (store *MapStore) Create(puppy Puppy) (int, error) { | ||
if puppy.Value < 0 { | ||
return -1, NewError(NegativeValue) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should be When an error is returned, the value of the other return parameters should be the zero value for their type. Otherwise people may start checking the wrong value for errors, or try to use those values thinking there might be some significance to the non-zero-value. |
||
} | ||
|
||
puppy.ID = store.uuid | ||
store.store[puppy.ID] = puppy | ||
store.uuid++ | ||
|
||
return puppy.ID, nil | ||
} | ||
|
||
// Read returns the puppy matching the provided uuid. | ||
// An empty Puppy struct is returned if the identifier does not exist. | ||
func (store *MapStore) Read(id int) (Puppy, error) { | ||
if _, ok := store.store[id]; ok { | ||
return store.store[id], nil | ||
} | ||
|
||
return Puppy{}, NewError(IDNotFound) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Two comments about this function (and I know my comments really apply to lab 6, not this one but here we are now):
unless you use a
|
||
} | ||
|
||
// Update modifies the puppy matching the provided uuid in the store with the provided Puppy struct. | ||
// It returns a bool whether a matching identifier was modified or not. | ||
func (store *MapStore) Update(id int, puppy Puppy) (bool, error) { | ||
if _, ok := store.store[id]; !ok { | ||
return false, NewError(IDNotFound) | ||
} | ||
if puppy.Value < 0 { | ||
return false, NewError(NegativeValue) | ||
} | ||
|
||
puppy.ID = id | ||
store.store[id] = puppy | ||
return true, nil | ||
} | ||
|
||
// Destroy removes the puppy matching the provided uuid from the store. | ||
// It returns a bool whether a matching identifier was deleted or not. | ||
func (store *MapStore) Destroy(id int) (bool, error) { | ||
if _, ok := store.store[id]; !ok { | ||
return false, NewError(IDNotFound) | ||
} | ||
|
||
delete(store.store, id) | ||
return true, nil | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
package main | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
"github.com/stretchr/testify/suite" | ||
) | ||
|
||
type StoreSuite struct { | ||
suite.Suite | ||
store Storer | ||
} | ||
|
||
func (suite *StoreSuite) TestCreate() { | ||
a := assert.New(suite.T()) | ||
|
||
id, error := suite.store.Create(Puppy{Breed: "Wolf", Color: "Grey", Value: 450}) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should be called |
||
a.Equal(id, 0) | ||
a.Equal(error, nil) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use |
||
} | ||
|
||
func (suite *StoreSuite) TestCreateSecond() { | ||
a := assert.New(suite.T()) | ||
|
||
id, error := suite.store.Create(Puppy{Breed: "Boxer", Color: "Brown", Value: 300}) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. s/error/err/ |
||
a.Equal(id, 1) | ||
a.Equal(error, nil) | ||
} | ||
|
||
func (suite *StoreSuite) TestCreateNegativeNumber() { | ||
a := assert.New(suite.T()) | ||
|
||
id, error := suite.store.Create(Puppy{Breed: "Wolf", Color: "Grey", Value: -100}) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. s/error/err/ |
||
a.Equal(id, -1) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
a.Equal(error, NewError(NegativeValue)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
} | ||
|
||
func (suite *StoreSuite) TestRead() { | ||
a := assert.New(suite.T()) | ||
|
||
data, error := suite.store.Read(0) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. s/error/err/ Going to stop saying this now. Please fix throughout |
||
a.Equal(data, Puppy{ID: 0, Breed: "Wolf", Color: "Grey", Value: 450}) | ||
a.Equal(error, nil) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Going to stop saying this now. Please fix throughout. |
||
} | ||
|
||
func (suite *StoreSuite) TestReadNonExistent() { | ||
a := assert.New(suite.T()) | ||
|
||
success, error := suite.store.Read(100) | ||
a.Equal(success, Puppy{}) | ||
a.Equal(error, NewError(IDNotFound)) | ||
} | ||
|
||
func (suite *StoreSuite) TestUpdate() { | ||
a := assert.New(suite.T()) | ||
|
||
success, error := suite.store.Update(0, Puppy{Breed: "Doberman", Color: "Black", Value: 500}) | ||
a.Equal(success, true) | ||
a.Equal(error, nil) | ||
|
||
data, error := suite.store.Read(0) | ||
a.Equal(data, Puppy{ID: 0, Breed: "Doberman", Color: "Black", Value: 500}) | ||
a.Equal(error, nil) | ||
} | ||
|
||
func (suite *StoreSuite) TestUpdateNonExistent() { | ||
a := assert.New(suite.T()) | ||
|
||
success, error := suite.store.Update(100, Puppy{Breed: "Doberman", Color: "Black", Value: 500}) | ||
a.Equal(success, false) | ||
a.Equal(error, NewError(IDNotFound)) | ||
} | ||
|
||
func (suite *StoreSuite) TestUpdateNegativeNumber() { | ||
a := assert.New(suite.T()) | ||
|
||
success, error := suite.store.Update(0, Puppy{Breed: "Doberman", Color: "Black", Value: -500}) | ||
a.Equal(success, false) | ||
a.Equal(error, NewError(NegativeValue)) | ||
} | ||
|
||
func (suite *StoreSuite) TestDestroy() { | ||
a := assert.New(suite.T()) | ||
|
||
success, error := suite.store.Destroy(1) | ||
a.Equal(success, true) | ||
a.Equal(error, nil) | ||
|
||
data, error := suite.store.Read(1) | ||
a.Equal(data, Puppy{}) | ||
a.Equal(error, NewError(IDNotFound)) | ||
} | ||
|
||
func (suite *StoreSuite) TestDestroyNonExistent() { | ||
a := assert.New(suite.T()) | ||
|
||
success, error := suite.store.Destroy(100) | ||
a.Equal(success, false) | ||
a.Equal(error, NewError(IDNotFound)) | ||
} | ||
|
||
func (suite *StoreSuite) TestIdIncrementOnDelete() { | ||
a := assert.New(suite.T()) | ||
id, _ := suite.store.Create(Puppy{Breed: "Greyhound", Color: "Light Brown", Value: 700}) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Rather than ignoring the error here, perhaps use
|
||
success, _ := suite.store.Destroy(id) | ||
a.Equal(success, true) | ||
|
||
newID, _ := suite.store.Create(Puppy{Breed: "Greyhound", Color: "Light Brown", Value: 700}) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Similar. You do not expect this |
||
a.Equal(newID, 3) | ||
} | ||
|
||
func TestStore(t *testing.T) { | ||
suite.Run(t, &StoreSuite{store: NewMapStore()}) | ||
suite.Run(t, &StoreSuite{store: NewSyncStore()}) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
package main | ||
|
||
// NewSyncStore returns a pointer to a new instance of the SyncStore struct which implements the Storer interface. | ||
func NewSyncStore() Storer { | ||
return &SyncStore{ | ||
uuid: 0, | ||
} | ||
} | ||
|
||
// Create increments the uuid and adds the provided Puppy struct to the store with this identifier. | ||
func (store *SyncStore) Create(puppy Puppy) (int, error) { | ||
if puppy.Value < 0 { | ||
return -1, NewError(NegativeValue) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. similar to mapstore - use a zero value for unspecified return values. |
||
} | ||
|
||
store.Lock() | ||
puppy.ID = store.uuid | ||
store.Store(puppy.ID, puppy) | ||
store.uuid++ | ||
store.Unlock() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The usual Go pattern is to put this right after the
This way if you later add an early return in the middle for error handling, you do not forget to unlock. |
||
|
||
return puppy.ID, nil | ||
} | ||
|
||
// Read returns the puppy matching the provided uuid. | ||
// An empty Puppy struct is returned if the identifier does not exist. | ||
func (store *SyncStore) Read(id int) (Puppy, error) { | ||
store.RLock() | ||
if value, ok := store.Load(id); ok { | ||
return value.(Puppy), nil | ||
} | ||
store.RUnlock() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a bug, because you are not unlocking the store inside the |
||
|
||
return Puppy{}, NewError(IDNotFound) | ||
} | ||
|
||
// Update modifies the puppy matching the provided uuid in the store with the provided Puppy struct. | ||
// It returns a bool whether a matching identifier was modified or not. | ||
func (store *SyncStore) Update(id int, puppy Puppy) (bool, error) { | ||
if _, ok := store.Load(id); !ok { | ||
return false, NewError(IDNotFound) | ||
} | ||
if puppy.Value < 0 { | ||
return false, NewError(NegativeValue) | ||
} | ||
|
||
puppy.ID = id | ||
store.Store(id, puppy) | ||
|
||
return true, nil | ||
} | ||
|
||
// Destroy removes the puppy matching the provided uuid from the store. | ||
// It returns a bool whether a matching identifier was deleted or not. | ||
func (store *SyncStore) Destroy(id int) (bool, error) { | ||
if _, ok := store.Load(id); !ok { | ||
return false, NewError(IDNotFound) | ||
} | ||
|
||
store.Lock() | ||
store.Delete(id) | ||
store.Unlock() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ditto. use the |
||
|
||
return true, nil | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
package main | ||
|
||
import "sync" | ||
|
||
type Puppy struct { | ||
ID int | ||
Breed string | ||
Color string | ||
Value int // cents | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Two ways you could convey this information without a comment:
Just a thought. |
||
} | ||
|
||
type Storer interface { | ||
Create(puppy Puppy) (int, error) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This commit on its own breaks the code. If you run The most minimal version of this change would be to change the implementation to always return |
||
Read(ID int) (Puppy, error) | ||
Update(ID int, puppy Puppy) (bool, error) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since the return value of |
||
Destroy(ID int) (bool, error) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same here |
||
} | ||
|
||
type MapStore struct { | ||
uuid int | ||
store map[int]Puppy | ||
} | ||
|
||
type SyncStore struct { | ||
uuid int | ||
sync.Map | ||
sync.RWMutex | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is not clear why you have decided to start at 1001 for the error codes, and also set
Unknown
to 1999. What if in future you need >1000 error codes? Since this is an enum, the actual values for the enum are not relevant, unless they map to some external system. In that case you should document that mapping. But if there is one, don't suggest there is one but you're not telling by using a different set of meaningless numbers to the default.I'd suggest starting with "Unknown = iota" and going up from there. That does not give you that artificial limit of the number of error codes when you do eventually get to more than 1000 error codes (which will come up in lab 22).
Perhaps you have chosen to use 1000+ so you get a consistent formatting width when converting the error to a string, but I'd suggest you do formatting at the place where formatting is specified (i.e.
[%04d]
)