Skip to content

Commit

Permalink
Merge branch 'lab7' of github.com:jimbosoft/go-course into lab7
Browse files Browse the repository at this point in the history
  • Loading branch information
Jim hejtmanek committed Jul 31, 2020
2 parents 83c9dd5 + 684a2e6 commit 2958f62
Show file tree
Hide file tree
Showing 7 changed files with 355 additions and 0 deletions.
73 changes: 73 additions & 0 deletions 06_puppy/jimbotech/mapstore.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package main

import (
"errors"
"fmt"

"github.com/google/uuid"
)

// MapStore stores puppies.
type MapStore map[uint32]*Puppy

// length used for testing.
func (s MapStore) length() int {
return len(s)
}

// ErrNotConstructed returned if the interface was called without
// first constructing the underlaying structure.
var ErrNotConstructed = errors.New("store not created")

// CreatePuppy add a puppy to storage
// but will modify the member ID.
func (s MapStore) CreatePuppy(p *Puppy) (uint32, error) {
if s == nil {
return 0, ErrNotConstructed
}
p.ID = uuid.New().ID()
sp := *p
s[p.ID] = &sp
return p.ID, nil
}

// ReadPuppy retrieve your puppy.
func (s MapStore) ReadPuppy(id uint32) (*Puppy, error) {
if s == nil {
return nil, ErrNotConstructed
}
val, found := s[id]
if !found {
return nil, fmt.Errorf("no puppy with ID %v found", id)
}
retVal := *val
return &retVal, nil
}

// UpdatePuppy update your puppy store.
func (s MapStore) UpdatePuppy(id uint32, puppy *Puppy) error {
if s == nil {
return ErrNotConstructed
}
if _, ok := s[id]; !ok {
return fmt.Errorf("no puppy with ID %v found", id)
}
puppy.ID = id
sp := *puppy
s[id] = &sp
return nil
}

// DeletePuppy remove the puppy from store.
func (s MapStore) DeletePuppy(id uint32) error {
if s == nil {
return ErrNotConstructed
}
delete(s, id)
return nil
}

// NewMapStore constructor creates the map.
func NewMapStore() MapStore {
return MapStore{}
}
23 changes: 23 additions & 0 deletions 06_puppy/jimbotech/mapstore_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package main

import (
"testing"

"github.com/stretchr/testify/assert"
)

// TestMapStoreWithoutContructor may not be required as it may not be
// possible to create an instance of that type without using the constructor
func TestMapStoreWithoutContructor(t *testing.T) {
var puppyStore MapStore
pup := Puppy{1, "kelpie", "brown", "indispensable"}

_, err := puppyStore.CreatePuppy(&pup)
assert.Equal(t, ErrNotConstructed, err)
err = puppyStore.UpdatePuppy(1, &pup)
assert.Equal(t, ErrNotConstructed, err)
_, err = puppyStore.ReadPuppy(1)
assert.Equal(t, ErrNotConstructed, err)
err = puppyStore.DeletePuppy(1)
assert.Equal(t, ErrNotConstructed, err)
}
18 changes: 18 additions & 0 deletions 06_puppy/jimbotech/puppy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package main

import (
"fmt"
"io"
"os"
)

var out io.Writer = os.Stdout

func main() {
var puppyStore Storer = NewMapStore()
pup := Puppy{Breed: "kelpie", Colour: "brown", Value: "indispensable"}
id, _ := puppyStore.CreatePuppy(&pup)
if pup, err := puppyStore.ReadPuppy(id); err == nil {
fmt.Fprintf(out, "retrieved: %v %v %v\n", pup.Breed, pup.Colour, pup.Value)
}
}
17 changes: 17 additions & 0 deletions 06_puppy/jimbotech/puppy_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package main

import (
"bytes"
"testing"
)

func TestMain(t *testing.T) {
expected := "retrieved: kelpie brown indispensable\n"
var buf bytes.Buffer
out = &buf
main()
actual := buf.String()
if actual != expected {
t.Errorf("expected %v, actual %v", expected, actual)
}
}
22 changes: 22 additions & 0 deletions 06_puppy/jimbotech/storer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package main

// Storer defines standard CRUD operations for Puppy
type Storer interface {
CreatePuppy(p *Puppy) (uint32, error)
ReadPuppy(ID uint32) (*Puppy, error)
UpdatePuppy(ID uint32, Puppy *Puppy) error
DeletePuppy(ID uint32) error
}

// Puppy stores puppy details.
type Puppy struct {
ID uint32
Breed string
Colour string
Value string
}

// mapTest used during testing to verify underlaying map changes
type mapTest interface {
length() int
}
137 changes: 137 additions & 0 deletions 06_puppy/jimbotech/storer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package main

import (
"fmt"
"testing"

"github.com/stretchr/testify/suite"
)

type storesSuite struct {
suite.Suite
store Storer
mapper mapTest
}

const brown = "brown"
const black = "black"
const grey = "grey"

func TestSuite(t *testing.T) {
suite.Run(t, &storesSuite{store: NewMapStore()})
suite.Run(t, &storesSuite{store: &SyncMapStore{}})
}

//SetupTest creates the correct empty map for each test
func (s *storesSuite) SetupTest() {
switch s.store.(type) {
case MapStore:
s.store = NewMapStore()
case *SyncMapStore:
s.store = &SyncMapStore{}
default:
s.Fail("Unknown Storer implementation")
}
s.mapper = s.store.(mapTest)
}

func (s *storesSuite) TestReadSuccess() {
pup := create(s)
// now check by reading the value back and compare
pup2, err2 := s.store.ReadPuppy(pup.ID)
s.Require().NoError(err2)
s.Equal(brown, pup2.Colour)
// modify the retured value to make sure the
// value in the store does not change
pup2.Colour = grey
pup3, err2 := s.store.ReadPuppy(pup.ID)
s.Require().NoError(err2)
s.Equal(brown, pup3.Colour)
s.NotEqual(pup2, pup3)
}

// TestCreateSuccess add to the store and verify
// by reading that it is in the store
func (s *storesSuite) TestCreateSuccess() {
pup := create(s)
// Now modify the original and make sure the
// value in the store will not change
pup.Colour = black
// now check by reading the value back and compare
pup2, err2 := s.store.ReadPuppy(pup.ID)
s.Require().NoError(err2)
s.Equal("kelpie", pup2.Breed)
s.Equal(brown, pup2.Colour)
s.Equal("indispensable", pup2.Value)
s.True(pup2.Colour == brown)
s.True(pup.Colour == black)
s.NotEqual(pup, pup2)
}

func create(s *storesSuite) *Puppy {
pup := Puppy{Breed: "kelpie", Colour: brown, Value: "indispensable"}
id, err := s.store.CreatePuppy(&pup)
s.Require().NoError(err)
s.Require().NotEqual(pup.ID, uint32(1))
s.Require().Equal(id, pup.ID, "Pup id must be set to actual id")
return &pup
}

func (s *storesSuite) TestUpdateSuccess() {
pup := create(s)
pup2 := Puppy{Breed: "kelpie", Colour: black, Value: "indispensable"}
err := s.store.UpdatePuppy(pup.ID, &pup2)
s.Require().NoError(err)
pup2.Colour = brown
// now check by reading the updated value back and compare
pup3, err2 := s.store.ReadPuppy(pup.ID)
if s.Nil(err2, "Reading back updated value should work") {
s.True(pup2.Colour == brown)
s.True(pup3.Colour == black)
s.NotEqual(pup2, *pup3)
}
}

//TestUpdateFailure checks the error returned when updating with an invalid id
func (s *storesSuite) TestUpdateFailure() {
create(s)
pup2 := Puppy{Breed: "kelpie", Colour: black, Value: "indispensable"}
err := s.store.UpdatePuppy(1, &pup2)
success := s.NotNil(err, "Update on id 1 should have failed")
if !success {
return
}
st := fmt.Sprintf("no puppy with ID %v found", 1)
s.Equal(st, err.Error())
}

func (s *storesSuite) TestDeleteSuccess() {
pup := create(s)
err := s.store.DeletePuppy(pup.ID)
s.Require().NoError(err)
_, err = s.store.ReadPuppy(pup.ID)
s.NotNil(err)
}

func (s *storesSuite) TestReadFailure() {
pup2, err := s.store.ReadPuppy(1)
s.Require().Nil(pup2)
s.Require().Error(err)
st := fmt.Sprintf("no puppy with ID %v found", 1)
s.Equal(st, err.Error())
}

func (s *storesSuite) TestMapChanges() {
s.Equal(0, s.mapper.length())
pup := Puppy{Breed: "kelpie", Colour: brown, Value: "high"}
id, err := s.store.CreatePuppy(&pup)
s.Require().Nil(err, "Create puppy failed")
s.Equal(1, s.mapper.length())
pup2 := Puppy{Breed: "kelpie", Colour: black, Value: "low"}
err = s.store.UpdatePuppy(id, &pup2)
s.Require().Nil(err, "Update puppy failed")
s.Equal(1, s.mapper.length())
err = s.store.DeletePuppy(id)
s.Require().Nil(err, "Delete puppy failed")
s.Equal(0, s.mapper.length())
}
65 changes: 65 additions & 0 deletions 06_puppy/jimbotech/sync_mapstore.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package main

import (
"fmt"
"sync"

"github.com/google/uuid"
)

// SyncMapStore stores puppies threadsafe.
type SyncMapStore struct {
sync.Map
}

// length is not concorrency safe. As the go doc says:
// Range does not necessarily correspond to any consistent snapshot of the
// Map's contents: no key will be visited more than once, but if the value
// for any key is stored or deleted concurrently, Range may reflect any
// mapping for that key from any point during the Range call.
//
func (s *SyncMapStore) length() int {
var length int
s.Range(func(key interface{}, value interface{}) bool {
length++
return true
})
return length
}

// CreatePuppy threadsafe adding a puppy to storage
// but will modify the member ID.
func (s *SyncMapStore) CreatePuppy(p *Puppy) (uint32, error) {
p.ID = uuid.New().ID()
sp := *p
s.Store(p.ID, &sp)
return p.ID, nil
}

// ReadPuppy threadsafe retrieval of your puppy.
func (s *SyncMapStore) ReadPuppy(id uint32) (*Puppy, error) {
val, found := s.Load(id)
if !found {
return nil, fmt.Errorf("no puppy with ID %v found", id)
}
retPup := *val.(*Puppy)
return &retPup, nil
}

// UpdatePuppy threadsafe update your puppy store.
func (s *SyncMapStore) UpdatePuppy(id uint32, puppy *Puppy) error {
_, found := s.Load(id)
if !found {
return fmt.Errorf("no puppy with ID %v found", id)
}
puppy.ID = id
sp := *puppy
s.Store(id, &sp)
return nil
}

// DeletePuppy threadsafe removal of the puppy from store.
func (s *SyncMapStore) DeletePuppy(id uint32) error {
s.Delete(id)
return nil
}

0 comments on commit 2958f62

Please sign in to comment.