bintest

package module
v3.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 6, 2023 License: MIT Imports: 25 Imported by: 1

README

Bintest

⚠ This is an open-sourced tool we use internally. You're welcome to use it, but we are unable to provide support.

Documentation

A set of tools for generating fake binaries that can be used for testing. A binary is compiled and then can be orchestrated from your test suite and later checked for assertions.

Mocks can communicate and respond in real-time with the tests that are calling them, which allows for testing complicated dependencies. See https://github.com/buildkite/agent/tree/master/bootstrap/integration for how we use it to test buildkite-agent's bootstrap process.

Mocks

Mocks are your typical mock object, but as an executable that your code can shell out to and then later test assertions on.

agent, err := bintest.NewMock("buildkite-agent")
if err != nil {
  t.Fatal(err)
}

agent.
  Expect("meta-data", "exists", "buildkite:git:commit").
  AndExitWith(1)
agent.
  Expect("meta-data", "set", mock.MatchAny()).
  AndExitWith(0)
agent.
  Expect("meta-data", "set", "buildkite:git:branch", mock.MatchAny()).
  AndExitWith(0)

agent.CheckAndClose(t)

Proxies

Proxies are what power Mocks.

// Compile a proxy for the git command that echos some debug
proxy, err := bintest.CompileProxy("git")
if err != nil {
  log.Fatal(err)
}

// call the proxy like a normal binary
go fmt.Println(exec.Command("git", "test", "arguments").CombinedOutput())

// handle invocations of the proxy binary
for call := range proxy.Ch {
  fmt.Fprintln(call.Stdout, "Llama party! 🎉")
  call.Exit(0)
}

// Llama party! 🎉

Credit

Inspired by bats-mock and go-binmock.

Documentation

Index

Examples

Constants

View Source
const (
	InfiniteTimes = -1
)
View Source
const (
	ServerEnvVar = `BINTEST_PROXY_SERVER`
)

Variables

View Source
var (
	Debug bool
)
View Source
var ErrNoExpectationsMatch = errors.New("No expectations match")

Functions

func AnyArguments

func AnyArguments() func(arg ...string) ArgumentsMatchResult

AnyArguments is a helper function for matching any argument set in WithMatcherFunc

func ExpectEnv

func ExpectEnv(t *testing.T, environ []string, expect ...string) error

ExpectEnv asserts that certain environment vars/values exist, otherwise an error is reported to T and a matching error is returned (for Before)

func FormatInterfaces

func FormatInterfaces(a []interface{}) string

FormatInterfaces formats a slice of interface{} as quoted comma-separated arguments

func FormatStrings

func FormatStrings(a []string) string

FormatStrings formats a slice of strings as quoted comma-separated arguments

func GetEnv

func GetEnv(key string, environ []string) (string, bool)

GetEnv returns the value for a given env in the invocation

func StopServer

func StopServer() error

StopServer stops the shared http server instance

Types

type Arguments

type Arguments []interface{}

func ArgumentsFromStrings

func ArgumentsFromStrings(s []string) Arguments

func (Arguments) Match

func (a Arguments) Match(x ...string) (result ArgumentsMatchResult)

func (Arguments) String

func (a Arguments) String() string

type ArgumentsMatchResult

type ArgumentsMatchResult struct {
	IsMatch     bool
	MatchCount  int
	Explanation string
}

type Call

type Call struct {
	PID  int
	Name string
	Args []string
	Env  []string
	Dir  string

	// Stdout is the output writer to send stdout to in the proxied binary
	Stdout io.WriteCloser `json:"-"`

	// Stderr is the output writer to send stdout to in the proxied binary
	Stderr io.WriteCloser `json:"-"`

	// Stdin is the input reader for stdin from the proxied binary
	Stdin io.ReadCloser `json:"-"`
	// contains filtered or unexported fields
}

Call is created for every call to the proxied binary

func (*Call) Exit

func (c *Call) Exit(code int)

Exit finishes the call and the proxied binary returns the exit code

func (*Call) Fatal

func (c *Call) Fatal(err error)

Fatal exits the call and returns the passed error. If it's a exec.ExitError the exit code is used

func (*Call) GetEnv

func (c *Call) GetEnv(key string) string

func (*Call) IsDone

func (c *Call) IsDone() bool

IsDone is a non-blocking thread-safe checks whether the call is done.

func (*Call) Passthrough

func (c *Call) Passthrough(path string)

Passthrough invokes another local binary and returns the results

func (*Call) PassthroughWithTimeout

func (c *Call) PassthroughWithTimeout(path string, timeout time.Duration)

PassthroughWithTimeout invokes another local binary and returns the results, if execution doesn't finish before the timeout the command is killed and an error is returned

type Client

type Client struct {
	Debug bool
	URL   string

	Args []string
	Dir  string
	Env  []string
	PID  int

	Stdin  io.ReadCloser
	Stdout io.WriteCloser
	Stderr io.WriteCloser
}

func NewClient

func NewClient(URL string) *Client

func NewClientFromEnv

func NewClientFromEnv() *Client

func (*Client) Run

func (c *Client) Run() int

Run the client, panics on error and returns an exit code on success

type Expectation

type Expectation struct {
	deadlock.RWMutex
	// contains filtered or unexported fields
}

Expectation is used for setting expectations

func (*Expectation) AndCallFunc

func (e *Expectation) AndCallFunc(f func(*Call)) *Expectation

AndCallFunc causes a middleware function to be called before invocation

func (*Expectation) AndExitWith

func (e *Expectation) AndExitWith(code int) *Expectation

AndExitWith causes the invoker to finish with an exit code of code

func (*Expectation) AndPassthroughToLocalCommand

func (e *Expectation) AndPassthroughToLocalCommand(path string) *Expectation

AndPassthroughToLocalCommand causes the invoker to defer to a local command

func (*Expectation) AndWriteToStderr

func (e *Expectation) AndWriteToStderr(s string) *Expectation

AndWriteToStderr causes the invoker to output s to stderr. This resets any passthrough path set

func (*Expectation) AndWriteToStdout

func (e *Expectation) AndWriteToStdout(s string) *Expectation

AndWriteToStdout causes the invoker to output s to stdout. This resets any passthrough path set

func (*Expectation) AtLeastOnce

func (e *Expectation) AtLeastOnce() *Expectation

AtLeastOnce expects a minimum invocations of 0 and a max of InfinityTimes

func (*Expectation) Check

func (e *Expectation) Check(t TestingT) bool

Check evaluates the expectation and outputs failures to the provided testing.T object

func (*Expectation) Exactly

func (e *Expectation) Exactly(expect int) *Expectation

Exactly expects exactly n invocations of this expectation

func (*Expectation) Max

func (e *Expectation) Max(expect int) *Expectation

Max expects a maximum of n invocations of this expectation, defaults to 1

func (*Expectation) Min

func (e *Expectation) Min(expect int) *Expectation

Min expects a minimum of n invocations of this expectation

func (*Expectation) NotCalled

func (e *Expectation) NotCalled() *Expectation

NotCalled is a shortcut for Exactly(0)

func (*Expectation) Once

func (e *Expectation) Once() *Expectation

Once is a shortcut for Exactly(1)

func (*Expectation) Optionally

func (e *Expectation) Optionally() *Expectation

Optionally is a shortcut for Min(0)

func (*Expectation) String

func (e *Expectation) String() string

func (*Expectation) WithAnyArguments

func (e *Expectation) WithAnyArguments() *Expectation

WithAnyArguments causes the expectation to accept any arguments via a MatcherFunc

func (*Expectation) WithMatcherFunc

func (e *Expectation) WithMatcherFunc(f func(arg ...string) ArgumentsMatchResult) *Expectation

WithMatcherFunc provides a custom matcher for argument sets, for instance matching variable amounts of arguments

func (*Expectation) WithStdin

func (e *Expectation) WithStdin(match interface{}) *Expectation

WithStdin sets an expectation on the stdin received by the command.

type ExpectationResult

type ExpectationResult struct {
	Arguments            []string
	Expectation          *Expectation
	ArgumentsMatchResult ArgumentsMatchResult
	CallCountMatch       bool
}

ExpectationResult is the result of a set of Arguments applied to an Expectation

func (ExpectationResult) Explain

func (r ExpectationResult) Explain() string

Explain returns an explanation of why the Expectation didn't match

type ExpectationResultSet

type ExpectationResultSet []ExpectationResult

ExpectationResultSet is a collection of ExpectationResult

func (ExpectationResultSet) ClosestMatch

func (r ExpectationResultSet) ClosestMatch() ExpectationResult

ClosestMatch returns the ExpectationResult that was the closest match (if not the exact) This is used for suggesting what the user might have meant

func (ExpectationResultSet) Match

func (r ExpectationResultSet) Match() (*Expectation, error)

Match returns the first Expectation that matches exactly, or ErrNoExpectationsMatch if none match.

type ExpectationSet

type ExpectationSet []*Expectation

ExpectationSet is a set of expectations

func (ExpectationSet) ForArguments

func (exp ExpectationSet) ForArguments(args ...string) (result ExpectationResultSet)

ForArguments applies arguments to the expectations and returns the results

type Invocation

type Invocation struct {
	Args        []string
	Env         []string
	Dir         string
	Expectation *Expectation
}

Invocation is a call to the binary

type Matcher

type Matcher interface {
	fmt.Stringer
	Match(s string) (bool, string)
}

func MatchAny

func MatchAny() Matcher

func MatchPattern

func MatchPattern(pattern string) Matcher

type MatcherFunc

type MatcherFunc struct {
	// contains filtered or unexported fields
}

func (MatcherFunc) Match

func (mf MatcherFunc) Match(s string) (bool, string)

func (MatcherFunc) String

func (mf MatcherFunc) String() string

type Mock

type Mock struct {
	deadlock.Mutex

	// Name of the binary
	Name string

	// Path to the bintest binary
	Path string
	// contains filtered or unexported fields
}

Mock provides a wrapper around a Proxy for testing

func NewMock

func NewMock(path string) (*Mock, error)

NewMock builds a new Mock, or an error if the bintest fails to compile

func NewMockFromTestMain

func NewMockFromTestMain(path string) (*Mock, error)

func (*Mock) Before

func (m *Mock) Before(f func(i Invocation) error) *Mock

Before adds a middleware that is run before the Invocation is dispatched

func (*Mock) Check

func (m *Mock) Check(t TestingT) bool

Check that all assertions are met and that there aren't invocations that don't match expectations

func (*Mock) CheckAndClose

func (m *Mock) CheckAndClose(t TestingT) error

func (*Mock) Close

func (m *Mock) Close() error

func (*Mock) Expect

func (m *Mock) Expect(args ...interface{}) *Expectation

Expect creates an expectation that the mock will be called with the provided args

func (*Mock) ExpectAll

func (m *Mock) ExpectAll(argSlices [][]interface{})

ExpectAll is a shortcut for adding lots of expectations

func (*Mock) IgnoreUnexpectedInvocations

func (m *Mock) IgnoreUnexpectedInvocations() *Mock

IgnoreUnexpectedInvocations allows for invocations without matching call expectations to just silently return 0 and no output

func (*Mock) PassthroughToLocalCommand

func (m *Mock) PassthroughToLocalCommand() *Mock

PassthroughToLocalCommand executes the mock name as a local command (looked up in PATH) and then passes the result as the result of the mock. Useful for assertions that commands happen, but where you want the command to actually be executed.

type Proxy

type Proxy struct {
	// Ch is the channel of calls
	Ch chan *Call

	// Path is the full path to the compiled binproxy file
	Path string

	// The server that the proxy uses to communicate with the binary
	Server *Server

	// A count of how many calls have been made
	CallCount int64
	// contains filtered or unexported fields
}

Proxy provides a way to programatically respond to invocations of a binary

func CompileProxy

func CompileProxy(path string) (*Proxy, error)

CompileProxy generates a mock binary at the provided path. If just a filename is provided a temp directory is created.

Example
// create a proxy for the git command that echos some debug
p, err := bintest.CompileProxy("git")
if err != nil {
	log.Fatal(err)
}

// call the proxy like a normal binary in the background
cmd := exec.Command(p.Path, "rev-parse")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr

// windows needs all the environment variables
cmd.Env = append(os.Environ(), `MY_MESSAGE=Llama party! 🎉`)

if err := cmd.Start(); err != nil {
	_ = p.Close()
	log.Fatal(err)
}

// handle invocations of the proxy binary
call := <-p.Ch
fmt.Fprintln(call.Stdout, call.GetEnv(`MY_MESSAGE`))
call.Exit(0)

// wait for the command to finish
_ = cmd.Wait()

if err := p.Close(); err != nil {
	log.Fatal(err)
}
Output:

Llama party! 🎉

func LinkTestBinaryAsProxy

func LinkTestBinaryAsProxy(path string) (*Proxy, error)

LinkTestBinaryAsProxy uses the current binary as a Proxy rather than compiling one directly This speeds things up considerably, but requires some code to be injected in TestMain

Example
// create a proxy for the git command that echos some debug
p, err := bintest.LinkTestBinaryAsProxy("git")
if err != nil {
	log.Fatal(err)
}

// call the proxy like a normal binary in the background
cmd := exec.Command(p.Path, "rev-parse")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr

// windows needs all the environment variables
cmd.Env = os.Environ()
cmd.Env = append(cmd.Env, p.Environ()...)
cmd.Env = append(cmd.Env, `MY_MESSAGE=Llama party! 🎉`)

if err := cmd.Start(); err != nil {
	log.Fatal(err)
}

// handle invocations of the proxy binary
call := <-p.Ch
fmt.Fprintln(call.Stdout, call.GetEnv(`MY_MESSAGE`))
call.Exit(0)

// wait for the command to finish
if err := cmd.Wait(); err != nil {
	log.Fatal(err)
}

// cleanup the proxy
if err := p.Close(); err != nil {
	log.Fatal(err)
}
Output:

Llama party! 🎉

func (*Proxy) Close

func (p *Proxy) Close() error

Close the proxy and remove the temp directory.

func (*Proxy) Environ

func (p *Proxy) Environ() []string

Environ returns environment variables required to invoke the proxy

type Server

type Server struct {
	net.Listener
	URL string
	// contains filtered or unexported fields
}

func StartServer

func StartServer() (*Server, error)

StartServer starts an instance of a proxy server

func (*Server) ServeHTTP

func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request)

type TestingT

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

TestingT is an interface for *testing.T

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL