Skip to content

Commit

Permalink
Feature/incrementor (#1)
Browse files Browse the repository at this point in the history
* counter added

* keep it simple + test

* fix doc
  • Loading branch information
wtask authored Mar 19, 2019
1 parent ad11b88 commit bd655cd
Show file tree
Hide file tree
Showing 3 changed files with 210 additions and 0 deletions.
68 changes: 68 additions & 0 deletions cyclic.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package counter

import (
"fmt"
"sync"
)

const (
// MaxInt - holds maximum int value for your platform
MaxInt = int(^uint(0) >> 1)
)

// CyclicIncrementor - step-by-step counter with limitation of its maximum value.
// After maximum is reached counter will reset into zero.
// You should use NewCyclicIncrementor() to create counter, but also can create counter like this:
// c := &counter.CyclicIncrementor{}
// But in that case, counter is not operational until its maximum value will be set:
// err := c.SetMaxValue(max)
// Also note, if counter is only declared as pointer:
// var c *CyclicIncrementor
// it is not really initialized and it cannot be used at this point.
type CyclicIncrementor struct {
mx sync.RWMutex // for value and max
value int
max int
}

// GetValue - return counter value
func (c *CyclicIncrementor) GetValue() int {
c.mx.RLock()
defer c.mx.RUnlock()
return c.value
}

// Inc - increment by 1 current value of counter. When value is reached max, counter will reset into zero.
func (c *CyclicIncrementor) Inc() {
c.mx.Lock()
if c.value < c.max {
c.value++
} else {
c.value = 0
}
c.mx.Unlock()
}

// SetMaxValue - change max allowed value for counter.
// Only positive integers allowed to set max value.
func (c *CyclicIncrementor) SetMaxValue(max int) error {
if max < 0 {
return fmt.Errorf("counter.CyclicIncrementor: invalid max value (%d)", max)
}
c.mx.Lock()
if c.value > max {
c.value = 0
}
c.max = max
c.mx.Unlock()
return nil
}

// NewCyclicIncrementor - return new cyclic counter with preassigned maximum value equals to MaxInt.
func NewCyclicIncrementor() (*CyclicIncrementor, error) {
c := &CyclicIncrementor{}
if err := c.SetMaxValue(MaxInt); err != nil {
return nil, err
}
return c, nil
}
140 changes: 140 additions & 0 deletions cyclic_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package counter

import (
"math/rand"
"sync"
"testing"
"time"
)

func TestUninitialized(t *testing.T) {
mustPanic := func(method string) {
if r := recover(); r == nil {
t.Errorf("Uninitialized counter (nil) did not panic for %s", method)
}
}
withoutPanic := func(method string) {
if r := recover(); r != nil {
t.Errorf("Uninitialized counter (nil) did panic for %s", method)
}
}
cases := []struct {
testMethod func(c *CyclicIncrementor)
}{
{func(c *CyclicIncrementor) { defer mustPanic("GetValue()"); c.GetValue() }},
{func(c *CyclicIncrementor) { defer mustPanic("Inc()"); c.Inc() }},
{func(c *CyclicIncrementor) {
defer withoutPanic("SetMaxValue(-1)")
if err := c.SetMaxValue(-1); err == nil {
t.Errorf("SetMaxValue(-1) must return error, but it is not")
}
}},
{func(c *CyclicIncrementor) { defer mustPanic("SetMaxValue(0)"); c.SetMaxValue(0) }},
{func(c *CyclicIncrementor) { defer mustPanic("SetMaxValue(1)"); c.SetMaxValue(1) }},
}
for _, c := range cases {
c.testMethod(nil)
}
}

func TestCyclicIncrementor(t *testing.T) {
// testing in normal flow, without concurrency
c, err := NewCyclicIncrementor()
if err != nil {
t.Errorf("Unexpected initial error: %s", err.Error())
}

if c.max != MaxInt {
t.Errorf("Unexpected initial maximum value (%d)", c.max)
}

if c.value != 0 {
t.Errorf("Unexpected initial current value (%d)", c.value)
}

up := 5
for i := 0; i < up; i++ {
c.Inc()
}
if c.GetValue() != up {
t.Errorf("Unexpected counter value (%d) after sequential incrementing (%d)", c.GetValue(), up)
}

err = c.SetMaxValue(10)
if err != nil {
t.Errorf("Unexpected SetMaxValue(10) error: %s", err.Error())
}
if c.GetValue() != 5 {
t.Errorf("Unexpected counter value after maximum changed (%d)", c.GetValue())
}

max := 4
err = c.SetMaxValue(max)
if err != nil {
t.Errorf("Unexpected SetMaxValue(%d) error: %s", max, err.Error())
}
if c.GetValue() != 0 {
t.Errorf("Counter value was not reset into zero (%d)", c.GetValue())
}
for i := 0; i < max; i++ {
c.Inc()
}
if c.GetValue() != max {
t.Errorf("Counter value (%d) was not reach allowed maximum (%d)", c.GetValue(), max)
}

c.Inc()
if c.GetValue() != 0 {
t.Errorf("Counter value (%d) was not reset into zero next after reaching maximum (%d)", c.GetValue(), max)
}
}

func TestCyclicIncrementorRWConcurrency(t *testing.T) {

read := func(c *CyclicIncrementor, times int) {
for i := 0; i < times; i++ {
c.GetValue()
randomSleep(0, 100*time.Millisecond)
}
}

write := func(c *CyclicIncrementor, times int) {
for i := 0; i < times; i++ {
c.Inc()
randomSleep(0, 100*time.Millisecond)
}
}

c, _ := NewCyclicIncrementor()

numWriters := 5
numIncrementsPerWriter := 10
expectedValue := numWriters * numIncrementsPerWriter
wg := sync.WaitGroup{}
wg.Add(numWriters * 2)
// if race will be detected test will fail
for i := 0; i < numWriters; i++ {
go func() {
write(c, numIncrementsPerWriter)
wg.Done()
}()
// also run concurrent reads
go func() {
read(c, 20)
wg.Done()
}()
}
wg.Wait()

if c.GetValue() != expectedValue {
t.Errorf("Unexpected counter value (%d)", c.GetValue())
}
}

func randomSleep(min, max time.Duration) {
if min > max {
min, max = max, min
}
r := rand.Int63n(int64(max - min))
time.Sleep(time.Duration(r) + min)
}
2 changes: 2 additions & 0 deletions doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Package counter provides different strategies of incrementing/decrementing values.
package counter

0 comments on commit bd655cd

Please sign in to comment.