From 0bb101ac842b02d904ce34a9de2ac7756f9e2079 Mon Sep 17 00:00:00 2001 From: "Derrick J. Wippler" Date: Mon, 13 Jul 2020 19:16:43 -0500 Subject: [PATCH] Added testutil.UntilPass and testutil.UntilConnect --- README.md | 77 ++++++++++++++++++++++++++++++++++++++++++ collections/README.md | 2 +- go.mod | 1 + testutil/until.go | 73 +++++++++++++++++++++++++++++++++++++++ testutil/until_test.go | 70 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 222 insertions(+), 1 deletion(-) create mode 100644 testutil/until.go create mode 100644 testutil/until_test.go diff --git a/README.md b/README.md index 91731b78..698b164e 100644 --- a/README.md +++ b/README.md @@ -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` @@ -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()) +} +``` \ No newline at end of file diff --git a/collections/README.md b/collections/README.md index 4056c750..c5823ffc 100644 --- a/collections/README.md +++ b/collections/README.md @@ -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` diff --git a/go.mod b/go.mod index fa64bb2a..5165af78 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/testutil/until.go b/testutil/until.go new file mode 100644 index 00000000..8a5386d5 --- /dev/null +++ b/testutil/until.go @@ -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() +} + diff --git a/testutil/until_test.go b/testutil/until_test.go new file mode 100644 index 00000000..a2739245 --- /dev/null +++ b/testutil/until_test.go @@ -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)) + }) +} \ No newline at end of file