Skip to content

Commit

Permalink
Merge pull request #71 from mailgun/thrawn/develop
Browse files Browse the repository at this point in the history
Added testutil.UntilPass and testutil.UntilConnect
  • Loading branch information
thrawn01 authored Jul 14, 2020
2 parents 2c142e9 + 0bb101a commit 84fa461
Show file tree
Hide file tree
Showing 5 changed files with 222 additions and 1 deletion.
77 changes: 77 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,8 @@ with the following
* `Keys()` - Get a list of keys at this point in time
* `Stats()` - Returns stats about the current state of the cache
* `AddWithTTL()` - Adds a value to the cache with a expiration time
* `Each()` - Concurrent non blocking access to each item in the cache
* `Map()` - Efficient blocking modification to each item in the cache

TTL is evaluated during calls to `.Get()` if the entry is past the requested TTL `.Get()`
removes the entry from the cache counts a miss and returns not `ok`
Expand Down Expand Up @@ -414,3 +416,78 @@ import "github.com/mailgun/holster/v3/syncutil"
// Tell the clients to quit
close(done)
```

## UntilPass
Functional test helper which will run a suite of tests until the entire suite
passes, or all attempts have been exhausted.

```go
import (
"github.com/mailgun/holster/v3/testutil"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/assert"
)

func TestUntilPass(t *testing.T) {
rand.Seed(time.Now().UnixNano())
var value string

ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodPost {
// Sleep some rand amount to time to simulate some
// async process happening on the server
time.Sleep(time.Duration(rand.Intn(10))*time.Millisecond)
// Set the value
value = r.FormValue("value")
} else {
fmt.Fprintln(w, value)
}
}))
defer ts.Close()

// Start the async process that produces a value on the server
http.PostForm(ts.URL, url.Values{"value": []string{"batch job completed"}})

// Keep running this until the batch job completes or attempts are exhausted
testutil.UntilPass(t, 10, time.Millisecond*100, func(t testutil.TestingT) {
r, err := http.Get(ts.URL)

// use of `require` will abort the current test here and tell UntilPass() to
// run again after 100 milliseconds
require.NoError(t, err)

// Or you can check if the assert returned true or not
if !assert.Equal(t, 200, r.StatusCode) {
return
}

b, err := ioutil.ReadAll(r.Body)
require.NoError(t, err)

assert.Equal(t, "batch job completed\n", string(b))
})
}
```

## UntilConnect
Waits until the test can connect to the TCP/HTTP server before continuing the test
```go
import (
"github.com/mailgun/holster/v3/testutil"
"golang.org/x/net/nettest"
"github.com/stretchr/testify/require"
)

func TestUntilConnect(t *testing.T) {
ln, err := nettest.NewLocalListener("tcp")
require.NoError(t, err)

go func() {
cn, err := ln.Accept()
require.NoError(t, err)
cn.Close()
}()
// Wait until we can connect, then continue with the test
testutil.UntilConnect(t, 10, time.Millisecond*100, ln.Addr().String())
}
```
2 changes: 1 addition & 1 deletion collections/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ with the following
* `Stats()` - Returns stats about the current state of the cache
* `AddWithTTL()` - Adds a value to the cache with a expiration time
* `Each()` - Concurrent non blocking access to each item in the cache
* `Map()` - Effecient blocking modification to each item in the cache
* `Map()` - Efficient blocking modification to each item in the cache

TTL is evaluated during calls to `.Get()` if the entry is past the requested TTL `.Get()`
removes the entry from the cache counts a miss and returns not `ok`
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ require (
go.uber.org/atomic v1.4.0 // indirect
go.uber.org/multierr v1.1.0 // indirect
go.uber.org/zap v1.10.0 // indirect
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 // indirect
google.golang.org/grpc v1.23.0
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
Expand Down
73 changes: 73 additions & 0 deletions testutil/until.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package testutil

import (
"fmt"
"net"
"time"
)

type TestingT interface {
Errorf(format string, args ...interface{})
FailNow()
}

type TestResults struct {
T TestingT
Failures []string
}

func (s *TestResults) Errorf(format string, args ...interface{}) {
s.Failures = append(s.Failures, fmt.Sprintf(format, args...))
}

func (s *TestResults) FailNow() {
s.Report(s.T)
s.T.FailNow()
}

func (s *TestResults) Report(t TestingT) {
for _, failure := range s.Failures {
t.Errorf(failure)
}
}

// Return true if the test eventually passed, false if the test failed
func UntilPass(t TestingT, attempts int, duration time.Duration, callback func(t TestingT)) bool {
results := TestResults{T: t}

for i := 0; i < attempts; i++ {
// Clear the failures before each attempt
results.Failures = nil

// Run the tests in the callback
callback(&results)

// If the test had no failures
if len(results.Failures) == 0 {
return true
}
// Sleep the duration
time.Sleep(duration)
}
// We have exhausted our attempts and should report the failures and exit
results.Report(t)
return false
}

// Continues to attempt connecting to the specified tcp address until either
// a successful connect or attempts are exhausted
func UntilConnect(t TestingT, a int, d time.Duration, addr string) {
for i := 0; i < a; i++ {
conn, err := net.Dial("tcp", addr)
if err != nil {
continue
}
conn.Close()
// Sleep the duration
time.Sleep(d)
return
}
t.Errorf("never connected to TCP server at '%s' after %d attempts", addr, a)
t.FailNow()
}

70 changes: 70 additions & 0 deletions testutil/until_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package testutil_test

import (
"fmt"
"io/ioutil"
"math/rand"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"time"

"github.com/mailgun/holster/v3/testutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/net/nettest"
)

func TestUntilConnect(t *testing.T) {
ln, err := nettest.NewLocalListener("tcp")
require.NoError(t, err)

go func() {
cn, err := ln.Accept()
require.NoError(t, err)
cn.Close()
}()
// Wait until we can connect, then continue with the test
testutil.UntilConnect(t, 10, time.Millisecond*100, ln.Addr().String())
}

func TestUntilPass(t *testing.T) {
rand.Seed(time.Now().UnixNano())
var value string

ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodPost {
// Sleep some rand amount to time to simulate some
// async process happening on the server
time.Sleep(time.Duration(rand.Intn(10))*time.Millisecond)
// Set the value
value = r.FormValue("value")
} else {
fmt.Fprintln(w, value)
}
}))
defer ts.Close()

// Start the async process that produces a value on the server
http.PostForm(ts.URL, url.Values{"value": []string{"batch job completed"}})

// Keep running this until the batch job completes or attempts are exhausted
testutil.UntilPass(t, 10, time.Millisecond*100, func(t testutil.TestingT) {
r, err := http.Get(ts.URL)

// use of `require` will abort the current test here and tell UntilPass() to
// run again after 100 milliseconds
require.NoError(t, err)

// Or you can check if the assert returned true or not
if !assert.Equal(t, 200, r.StatusCode) {
return
}

b, err := ioutil.ReadAll(r.Body)
require.NoError(t, err)

assert.Equal(t, "batch job completed\n", string(b))
})
}

0 comments on commit 84fa461

Please sign in to comment.