blob: f9f0a59d060bf623e5fbc9da7a5d4eab3fd4107f [file] [log] [blame]
// Package executil provides a mostly transparent way to make os/exec testable. It is inspired by
// https://npf.io/2015/06/testing-exec-command/ (which was inspired by the standard library's tests
// of os/exec). Basically, the helpers in this package replace a call to an arbitrary executable
// (and arguments) with a call to the underlying test binary, with a flag to run exactly one test.
// This test can then be a fake implementation of the binary, do assertions on the arguments, etc.
//
// See executil_test.go for example usages.
package executil
import (
"context"
"os"
"os/exec"
"sync"
)
const (
// OverrideEnvironmentVariable is the environment variable that will be set if a test has been
// invoked via CommandContext below and it should behave as if it is faking a call to an
// executable. The value it is set to should be considered arbitrary and not relied upon.
OverrideEnvironmentVariable = "SKIA_INFRA_OVERRIDE_TEST"
// This is the key used in context.Value to correspond to a *fakeTestTracker object.
overrideKey = "skia_infra_override_cmd"
)
// WithFakeTests returns a context.Context loaded with a special Value containing the given test
// names. When this Context is passed into this package's CommandContext, faked *exec.Cmd objects
// will be returned using the given test names. The first call to CommandContext will be faked
// with the first value of fakeTestNames, the second call to CommandContext will use the
// second value of fakeTestNames and so on. This panics if the provided context was one that
// already has fake tests associated with it.
func WithFakeTests(parent context.Context, fakeTestNames ...string) context.Context {
if _, ok := parent.Value(overrideKey).(*fakeTestTracker); ok {
panic("parent context already has fake tests associated with it")
}
return context.WithValue(parent, overrideKey, &fakeTestTracker{
index: 0,
fakeTestNames: fakeTestNames,
})
}
// FakeTestsContext is a convenient wrapper around WithFakeTests using context.Background().
func FakeTestsContext(fakeTestNames ...string) context.Context {
return WithFakeTests(context.Background(), fakeTestNames...)
}
// fakeTestTracker keeps track of which test we should fake out next. We have this be a struct and
// store the pointer to this struct in the ctx.Value so we can mutate the value without having
// to return a new context or something more complex. Contexts are meant to be thread safe, so this
// object has a mutex to avoid problems when being used synchronously, although in practice using
// this package in a multi-threaded fashion would likely lead to flaky tests.
type fakeTestTracker struct {
index int
fakeTestNames []string
mutex sync.Mutex
}
// CommandContext looks for a special value on the provided context.Context (see WithFakeTests).
// If that value exists, it will use the next fake test value and return a faked *exec.Cmd. It
// panics if there are not enough fake tests that were provided to the original context. If the
// special value does not exist, it is a passthrough to os/exec.CommandContext.
func CommandContext(ctx context.Context, cmd string, args ...string) *exec.Cmd {
if override, ok := ctx.Value(overrideKey).(*fakeTestTracker); ok {
override.mutex.Lock()
defer override.mutex.Unlock()
// We are going to shell out to the current test executable...
testBinary := os.Args[0]
// ...and tell it to run the next faked test.
if override.index >= len(override.fakeTestNames) {
panic("Not enough fake tests provided")
}
fakeTest := override.fakeTestNames[override.index]
override.index++
// fakeTest is where the client has put their fake implementation of the given command.
argsWithOverride := []string{"-test.run=" + fakeTest, "--", cmd}
argsWithOverride = append(argsWithOverride, args...)
fakedCmd := exec.CommandContext(ctx, testBinary, argsWithOverride...)
fakedCmd.Env = []string{OverrideEnvironmentVariable + "=1"}
return fakedCmd
}
// Did not find special Context value, so fall back to default impl
return exec.CommandContext(ctx, cmd, args...)
}
// FakeCommandsReturned returns the count of how many times CommandContext was called using the
// given context. This is a proxy for the number of fake commands run.
func FakeCommandsReturned(ctx context.Context) int {
if override, ok := ctx.Value(overrideKey).(*fakeTestTracker); ok {
override.mutex.Lock()
defer override.mutex.Unlock()
return override.index
}
panic("A Context was passed in that was not produced by the executil package.")
}
// OriginalArgs returns the original arguments passed into a test function. Concretely, it looks
// at the osArgs and strips off the first 3 (the test binary, the test to run, and "--")
func OriginalArgs() []string {
return os.Args[3:]
}
// IsCallingFakeCommand returns whether the current process is a test process that's running a
// mocked-out CLI invocation. This should be called at the beginning of each Test_FakeExe_... test
// and trigger an early return if false.
func IsCallingFakeCommand() bool {
return os.Getenv(OverrideEnvironmentVariable) != ""
}