Add gotestyourself/poll

Signed-off-by: Daniel Nephin <dnephin@docker.com>
This commit is contained in:
Daniel Nephin 2017-09-01 14:54:03 -04:00
parent 0426ea1443
commit 683b6226ed
7 changed files with 155 additions and 20 deletions

View File

@ -21,7 +21,7 @@ github.com/gogo/protobuf v0.4
github.com/golang/protobuf 7a211bcf3bce0e3f1d74f9894916e6f116ae83b4 github.com/golang/protobuf 7a211bcf3bce0e3f1d74f9894916e6f116ae83b4
github.com/gorilla/context v1.1 github.com/gorilla/context v1.1
github.com/gorilla/mux v1.1 github.com/gorilla/mux v1.1
github.com/gotestyourself/gotestyourself v1.0.0 github.com/gotestyourself/gotestyourself v1.1.0
github.com/inconshreveable/mousetrap 76626ae9c91c4f2a10f34cad8ce83ea42c93bb75 github.com/inconshreveable/mousetrap 76626ae9c91c4f2a10f34cad8ce83ea42c93bb75
github.com/mattn/go-shellwords v1.0.3 github.com/mattn/go-shellwords v1.0.3
github.com/Microsoft/go-winio v0.4.4 github.com/Microsoft/go-winio v0.4.4

View File

@ -18,10 +18,11 @@ patterns.
a program to summarize `go test` output and test failures a program to summarize `go test` output and test failures
* [icmd](http://godoc.org/github.com/gotestyourself/gotestyourself/icmd) - * [icmd](http://godoc.org/github.com/gotestyourself/gotestyourself/icmd) -
execute binaries and test the output execute binaries and test the output
* [poll](http://godoc.org/github.com/gotestyourself/gotestyourself/poll) -
test asynchronous code by polling until a desired state is reached
* [skip](http://godoc.org/github.com/gotestyourself/gotestyourself/skip) - * [skip](http://godoc.org/github.com/gotestyourself/gotestyourself/skip) -
skip tests based on conditions skip tests based on conditions
## Related ## Related
* [testify/assert](https://godoc.org/github.com/stretchr/testify/assert) and * [testify/assert](https://godoc.org/github.com/stretchr/testify/assert) and

View File

@ -37,8 +37,8 @@ func update(t require.TestingT, filename string, actual []byte) {
} }
// Assert compares the actual content to the expected content in the golden file. // Assert compares the actual content to the expected content in the golden file.
// If `--update-golden` is set then the actual content is written to the golden // If the `-test.update-golden` flag is set then the actual content is written
// file. // to the golden file.
// Returns whether the assertion was successful (true) or not (false) // Returns whether the assertion was successful (true) or not (false)
func Assert(t require.TestingT, actual string, filename string, msgAndArgs ...interface{}) bool { func Assert(t require.TestingT, actual string, filename string, msgAndArgs ...interface{}) bool {
expected := Get(t, filename) expected := Get(t, filename)
@ -60,8 +60,8 @@ func Assert(t require.TestingT, actual string, filename string, msgAndArgs ...in
} }
// AssertBytes compares the actual result to the expected result in the golden // AssertBytes compares the actual result to the expected result in the golden
// file. If `--update-golden` is set then the actual content is written to the // file. If the `-test.update-golden` flag is set then the actual content is
// golden file. // written to the golden file.
// Returns whether the assertion was successful (true) or not (false) // Returns whether the assertion was successful (true) or not (false)
// nolint: lll // nolint: lll
func AssertBytes(t require.TestingT, actual []byte, filename string, msgAndArgs ...interface{}) bool { func AssertBytes(t require.TestingT, actual []byte, filename string, msgAndArgs ...interface{}) bool {

View File

@ -18,10 +18,8 @@ type testingT interface {
Fatalf(string, ...interface{}) Fatalf(string, ...interface{})
} }
const (
// None is a token to inform Result.Assert that the output should be empty // None is a token to inform Result.Assert that the output should be empty
None string = "<NOTHING>" const None string = "[NOTHING]"
)
type lockedBuffer struct { type lockedBuffer struct {
m sync.RWMutex m sync.RWMutex
@ -170,8 +168,7 @@ func (r *Result) Combined() string {
return r.outBuffer.String() + r.errBuffer.String() return r.outBuffer.String() + r.errBuffer.String()
} }
// SetExitError sets Error and ExitCode based on Error func (r *Result) setExitError(err error) {
func (r *Result) SetExitError(err error) {
if err == nil { if err == nil {
return return
} }
@ -196,7 +193,7 @@ func Command(command string, args ...string) Cmd {
} }
// RunCmd runs a command and returns a Result // RunCmd runs a command and returns a Result
func RunCmd(cmd Cmd, cmdOperators ...func(*Cmd)) *Result { func RunCmd(cmd Cmd, cmdOperators ...CmdOp) *Result {
for _, op := range cmdOperators { for _, op := range cmdOperators {
op(&cmd) op(&cmd)
} }
@ -207,7 +204,7 @@ func RunCmd(cmd Cmd, cmdOperators ...func(*Cmd)) *Result {
return WaitOnCmd(cmd.Timeout, result) return WaitOnCmd(cmd.Timeout, result)
} }
// RunCommand parses a command line and runs it, returning a result // RunCommand runs a command with default options, and returns a result
func RunCommand(command string, args ...string) *Result { func RunCommand(command string, args ...string) *Result {
return RunCmd(Command(command, args...)) return RunCmd(Command(command, args...))
} }
@ -218,7 +215,7 @@ func StartCmd(cmd Cmd) *Result {
if result.Error != nil { if result.Error != nil {
return result return result
} }
result.SetExitError(result.Cmd.Start()) result.setExitError(result.Cmd.Start())
return result return result
} }
@ -253,7 +250,7 @@ func buildCmd(cmd Cmd) *Result {
// only wait until the timeout. // only wait until the timeout.
func WaitOnCmd(timeout time.Duration, result *Result) *Result { func WaitOnCmd(timeout time.Duration, result *Result) *Result {
if timeout == time.Duration(0) { if timeout == time.Duration(0) {
result.SetExitError(result.Cmd.Wait()) result.setExitError(result.Cmd.Wait())
return result return result
} }
@ -271,7 +268,7 @@ func WaitOnCmd(timeout time.Duration, result *Result) *Result {
} }
result.Timeout = true result.Timeout = true
case err := <-done: case err := <-done:
result.SetExitError(err) result.setExitError(err)
} }
return result return result
} }

View File

@ -7,9 +7,9 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
) )
// GetExitCode returns the ExitStatus of a process from the error returned by // getExitCode returns the ExitStatus of a process from the error returned by
// exec.Run(). If the exit status could not be parsed an error is returned. // exec.Run(). If the exit status could not be parsed an error is returned.
func GetExitCode(err error) (int, error) { func getExitCode(err error) (int, error) {
if exiterr, ok := err.(*exec.ExitError); ok { if exiterr, ok := err.(*exec.ExitError); ok {
if procExit, ok := exiterr.Sys().(syscall.WaitStatus); ok { if procExit, ok := exiterr.Sys().(syscall.WaitStatus); ok {
return procExit.ExitStatus(), nil return procExit.ExitStatus(), nil
@ -22,7 +22,7 @@ func processExitCode(err error) (exitCode int) {
if err == nil { if err == nil {
return 0 return 0
} }
exitCode, exiterr := GetExitCode(err) exitCode, exiterr := getExitCode(err)
if exiterr != nil { if exiterr != nil {
// TODO: Fix this so we check the error's text. // TODO: Fix this so we check the error's text.
// we've failed to retrieve exit code, so we set it to 127 // we've failed to retrieve exit code, so we set it to 127

View File

@ -0,0 +1,4 @@
package icmd
// CmdOp is an operation which modified a Cmd structure used to execute commands
type CmdOp func(*Cmd)

View File

@ -0,0 +1,133 @@
/*Package poll provides tools for testing asynchronous code.
*/
package poll
import (
"fmt"
"time"
)
// TestingT is the subset of testing.T used by WaitOn
type TestingT interface {
LogT
Fatalf(format string, args ...interface{})
}
// LogT is a logging interface that is passed to the WaitOn check function
type LogT interface {
Log(args ...interface{})
Logf(format string, args ...interface{})
}
// Settings are used to configure the behaviour of WaitOn
type Settings struct {
// Timeout is the maximum time to wait for the condition. Defaults to 10s
Timeout time.Duration
// Delay is the time to sleep between checking the condition. Detaults to
// 1ms
Delay time.Duration
}
func defaultConfig() *Settings {
return &Settings{Timeout: 10 * time.Second, Delay: time.Millisecond}
}
// SettingOp is a function which accepts and modifies Settings
type SettingOp func(config *Settings)
// WithDelay sets the delay to wait between polls
func WithDelay(delay time.Duration) SettingOp {
return func(config *Settings) {
config.Delay = delay
}
}
// WithTimeout sets the timeout
func WithTimeout(timeout time.Duration) SettingOp {
return func(config *Settings) {
config.Timeout = timeout
}
}
// Result of a check performed by WaitOn
type Result interface {
// Error indicates that the check failed and polling should stop, and the
// the has failed
Error() error
// Done indicates that polling should stop, and the test should proceed
Done() bool
// Message provides the most recent state when polling has not completed
Message() string
}
type result struct {
done bool
message string
err error
}
func (r result) Done() bool {
return r.done
}
func (r result) Message() string {
return r.message
}
func (r result) Error() error {
return r.err
}
// Continue returns a Result that indicates to WaitOn that it should continue
// polling. The message text will be used as the failure message if the timeout
// is reached.
func Continue(message string, args ...interface{}) Result {
return result{message: fmt.Sprintf(message, args...)}
}
// Success returns a Result where Done() returns true, which indicates to WaitOn
// that it should stop polling and exit without an error.
func Success() Result {
return result{done: true}
}
// Error returns a Result that indicates to WaitOn that it should fail the test
// and stop polling.
func Error(err error) Result {
return result{err: err}
}
// WaitOn a condition or until a timeout. Poll by calling check and exit when
// check returns a done Result. To fail a test and exit polling with an error
// return a error result.
func WaitOn(t TestingT, check func(t LogT) Result, pollOps ...SettingOp) {
config := defaultConfig()
for _, pollOp := range pollOps {
pollOp(config)
}
var lastMessage string
after := time.After(config.Timeout)
chResult := make(chan Result)
for {
go func() {
chResult <- check(t)
}()
select {
case <-after:
if lastMessage == "" {
lastMessage = "first check never completed"
}
t.Fatalf("timeout hit after %s: %s", config.Timeout, lastMessage)
case result := <-chResult:
switch {
case result.Error() != nil:
t.Fatalf("polling check failed: %s", result.Error())
case result.Done():
return
}
time.Sleep(config.Delay)
lastMessage = result.Message()
}
}
}