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

Puppy store error handling #557

Merged
merged 1 commit into from
Jul 18, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions 07_errors/runnerdave/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package main
runnerdave marked this conversation as resolved.
Show resolved Hide resolved

import "fmt"
runnerdave marked this conversation as resolved.
Show resolved Hide resolved

// Error codes
const (
// ErrUnknown is used when an unknown error occurred
ErrUnknown uint16 = iota
// ErrInvalidValue is used when the value for the puppy is negative
ErrInvalidValue
// ErrIDNotFound is used when attempting to read a non-existing entry
ErrIDNotFound
)

// Error struct to identify errors in Puppy store
type Error struct {
Code uint16 `json:"code"`
Message string `json:"message"`
}

func (e *Error) Error() string {
return fmt.Sprintf("%d: %s", e.Code, e.Message)
}

// Errorf creates a new Error with formatting
func Errorf(code uint16, format string, args ...interface{}) *Error {
return &Error{code, fmt.Sprintf(format, args...)}
}

func validateValue(value float32) error {
if value < 0 {
return Errorf(ErrInvalidValue, "puppy has invalid value (%f)", value)
}
return nil
}
30 changes: 30 additions & 0 deletions 07_errors/runnerdave/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package main

import (
"fmt"
"io"
runnerdave marked this conversation as resolved.
Show resolved Hide resolved
"os"
runnerdave marked this conversation as resolved.
Show resolved Hide resolved
)

var out io.Writer = os.Stdout

func main() {
puppy1 := Puppy{ID: 11, Breed: "Chihuahua", Colour: "Brown", Value: 12.30}
puppy2 := Puppy{ID: 11, Breed: "Chihuahua", Colour: "Brown", Value: 10.30}

mapStore := NewMapStore()
mapCreateErr := mapStore.CreatePuppy(&puppy1)

puppyMap, _ := mapStore.ReadPuppy(11)
runnerdave marked this conversation as resolved.
Show resolved Hide resolved
fmt.Fprintf(out, "Puppy created in map of Breed:%s, errors at creation:%v\n", puppyMap.Breed, mapCreateErr)

syncStore := NewSyncStore()
syncCreateErr := syncStore.CreatePuppy(&puppy1)
syncUpdateErr := syncStore.UpdatePuppy(11, &puppy2)
puppySync, _ := syncStore.ReadPuppy(11)
fmt.Fprintf(out, "Puppy created in sync of Breed:%s, value updated to:%f, error at creation:%v, error in update:%v\n",
puppySync.Breed, puppySync.Value, syncCreateErr, syncUpdateErr)

puppyMap, err := mapStore.ReadPuppy(12)
fmt.Fprint(out, err)
}
25 changes: 25 additions & 0 deletions 07_errors/runnerdave/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package main

import (
"bytes"
"strconv"
"testing"
)

func TestMainOutput(t *testing.T) {
var buf bytes.Buffer
out = &buf

main()

expected := strconv.Quote(`Puppy created in map of Breed:Chihuahua, errors at creation:<nil>
Puppy created in sync of Breed:Chihuahua, value updated to:10.300000, error at creation:<nil>, error in update:<nil>
2: puppy with ID:12 not found`)
actual := strconv.Quote(buf.String())
t.Logf("expected:%s", expected)
t.Logf("actual:%s", actual)

if expected != actual {
t.Errorf("Unexpected output in main()")
}
}
52 changes: 52 additions & 0 deletions 07_errors/runnerdave/map_store.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package main

// MapStore represents a simple map storage for the Puppy store
type MapStore struct {
puppies map[uint16]Puppy
}

// NewMapStore creates a new in-memory store with map intialised
func NewMapStore() *MapStore {
return &MapStore{puppies: map[uint16]Puppy{}}
}

// CreatePuppy saves new puppy if not in store, if it is already returns error
func (m *MapStore) CreatePuppy(p *Puppy) error {
if err := validateValue(p.Value); err != nil {
return err
}
if _, ok := m.puppies[p.ID]; ok {
return Errorf(ErrUnknown, "puppy with id %d already exists", p.ID)
}
m.puppies[p.ID] = *p
return nil
}

// ReadPuppy reads store by Puppy ID
func (m *MapStore) ReadPuppy(id uint16) (Puppy, error) {
if puppy, ok := m.puppies[id]; ok {
return puppy, nil
}
return Puppy{}, Errorf(ErrIDNotFound, "puppy with ID:%d not found", id)
}

// UpdatePuppy updates puppy with new value if ID present otherwise error
func (m *MapStore) UpdatePuppy(id uint16, p *Puppy) error {
if err := validateValue(p.Value); err != nil {
return err
}
if _, ok := m.puppies[id]; !ok {
return Errorf(ErrIDNotFound, "puppy with ID:%d not found", id)
}
m.puppies[id] = *p
return nil
}

// DeletePuppy deletes a puppy by id from the store
func (m *MapStore) DeletePuppy(id uint16) error {
if _, ok := m.puppies[id]; ok {
Copy link
Contributor

Choose a reason for hiding this comment

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

Usual Go style would be to test for !ok and return an error, leaving the non-error case at the left edge of the function: https://medium.com/@matryer/line-of-sight-in-code-186dd7cdea88

In ReadPuppy you couldn't really do that because of the scope of the puppy variable was the if block, but in this case you are ignoring that variable.

But this function is small enough that it doesn't really matter, so don't worry about changing it unless you really want to. Just keep it in mind in future - if it could go either way, keep the happy path at the left edge.

delete(m.puppies, id)
return nil
}
return Errorf(ErrIDNotFound, "puppy with ID:%d not found", id)
}
193 changes: 193 additions & 0 deletions 07_errors/runnerdave/storer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
package main

import (
"testing"

tassert "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
)

var (
puppy1 = func() Puppy {
return Puppy{ID: 11, Breed: "Chihuahua", Colour: "Brown", Value: 12.30}
}
puppy2 = func() Puppy {
return Puppy{ID: 12, Breed: "Cacri", Colour: "Undefined", Value: 1.30}
}
puppy3 = func() Puppy {
return Puppy{ID: 12, Breed: "Imaginary", Colour: "Undefined", Value: -1.30}
}
)

type storerSuite struct {
suite.Suite
store Storer
storerType func() Storer
}

func (s *storerSuite) TestUpdatePuppyIDDoesNotExist() {
// given
assert := tassert.New(s.T())
testPuppy := puppy1()

// when
err := s.store.UpdatePuppy(13, &testPuppy)

// then
assert.Error(err, "Should produce an error if id not found")
serr, ok := err.(*Error)
assert.True(ok)
assert.Equal(uint16(0x2), serr.Code)
}

func (s *storerSuite) TestUpdatePuppy() {
// given
assert := tassert.New(s.T())
testPuppy := puppy1()
targetPuppy := puppy2()
cerr := s.store.CreatePuppy(&testPuppy)
r := require.New(s.T())
r.NoError(cerr, "Create should not produce an error")

// when
uerr := s.store.UpdatePuppy(11, &targetPuppy)
runnerdave marked this conversation as resolved.
Show resolved Hide resolved

// then
r.NoError(uerr, "Should be able to update store")
updatedPuppy, err := s.store.ReadPuppy(11)
r.NoError(err, "Should be able to read updated puppy")
assert.Equal(targetPuppy, updatedPuppy, "Updated puppy should be equal to puppy2")
}

func (s *storerSuite) TestReadPuppy() {
// given
assert := tassert.New(s.T())
testPuppy := puppy1()
cerr := s.store.CreatePuppy(&testPuppy)
r := require.New(s.T())
r.NoError(cerr, "Create should not produce an error")

// when
newPuppy, err := s.store.ReadPuppy(11)

// then
r.NoError(err, "Should be able to read a newly added puppy")
assert.Equal(testPuppy, newPuppy, "Newly added puppy should be equal to test puppy")
}

func (s *storerSuite) TestReadNonExistingPuppy() {
// given
assert := tassert.New(s.T())
r := require.New(s.T())

// when
_, err := s.store.ReadPuppy(12)

// then
r.Error(err, "Should produce an error when puppy is not found")
serr, ok := err.(*Error)
assert.True(ok)
assert.Equal(uint16(0x2), serr.Code)
}

func (s *storerSuite) TestDeletePuppy() {
// given
assert := tassert.New(s.T())
testPuppy := puppy1()
cerr := s.store.CreatePuppy(&testPuppy)
r := require.New(s.T())
r.NoError(cerr, "Create should not produce an error")

// when
err := s.store.DeletePuppy(11)

// then
r.NoError(err, "Should be able to delete a newly added puppy")
_, rerr := s.store.ReadPuppy(11)
serr, ok := rerr.(*Error)
assert.True(ok)
assert.Equal(uint16(0x2), serr.Code)
}

func (s *storerSuite) TestDeleteNonExistingPuppy() {
// given
assert := tassert.New(s.T())
r := require.New(s.T())

// when
err := s.store.DeletePuppy(11)

// then
r.Error(err, "Should not be able to delete a non existing puppy")
serr, ok := err.(*Error)
assert.True(ok)
assert.Equal(uint16(0x2), serr.Code)
}

func (s *storerSuite) TestCreateExistingPuppy() {
// given
assert := tassert.New(s.T())
testPuppy := puppy1()
cerr := s.store.CreatePuppy(&testPuppy)
r := require.New(s.T())
r.NoError(cerr, "Create should not produce an error")

// when
err := s.store.CreatePuppy(&testPuppy)

// then
assert.Error(err, "Should not be able to create twice a the same puppy")
}

func (s *storerSuite) TestCreatePuppyWithInvalidValue() {
// given
assert := tassert.New(s.T())
testPuppy := puppy3()
r := require.New(s.T())

// when
createError := s.store.CreatePuppy(&testPuppy)

// then
r.Error(createError, "Should not allow to create a puppy with invalid value")
serr, ok := createError.(*Error)
assert.True(ok)
assert.Equal(uint16(0x1), serr.Code)
}

func (s *storerSuite) TestUpdatePuppyWithInvalidValue() {
// given
assert := tassert.New(s.T())
testPuppy := puppy1()
updatePuppy := puppy3()
createError := s.store.CreatePuppy(&testPuppy)
r := require.New(s.T())
r.NoError(createError, "Create should not produce an error")

// when
uerr := s.store.UpdatePuppy(11, &updatePuppy)

// then
r.Error(uerr, "Should not allow to update a puppy with invalid value")
serr, ok := uerr.(*Error)
assert.True(ok)
assert.Equal(uint16(0x1), serr.Code)
}

func (s *storerSuite) SetupTest() {
s.store = s.storerType()
}

func TestStorer(t *testing.T) {
syncSuite := storerSuite{
store: NewSyncStore(),
Copy link
Contributor

Choose a reason for hiding this comment

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

Since SetupTest() sets store at the start of every test, this does not need to be initialised here - you can just leave it as the zero value (nil).

storerType: func() Storer { return NewSyncStore() },
}
mapSuite := storerSuite{
store: NewMapStore(),
Copy link
Contributor

Choose a reason for hiding this comment

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

Ditto

storerType: func() Storer { return NewMapStore() },
}
suite.Run(t, &syncSuite)
suite.Run(t, &mapSuite)
}
Loading