120 lines
2.5 KiB
Go
120 lines
2.5 KiB
Go
|
|
package runner
|
||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"fmt"
|
||
|
|
"os/exec"
|
||
|
|
)
|
||
|
|
|
||
|
|
// Spec describes a single command to run.
|
||
|
|
type Spec struct {
|
||
|
|
Name string
|
||
|
|
Args []string
|
||
|
|
Dir string
|
||
|
|
Env []string
|
||
|
|
Stdin []byte
|
||
|
|
}
|
||
|
|
|
||
|
|
// Runner runs external commands. Production code uses Real; tests use Fake.
|
||
|
|
type Runner interface {
|
||
|
|
Run(ctx context.Context, s Spec) ([]byte, error)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Real executes via os/exec and returns combined stdout+stderr.
|
||
|
|
type Real struct{}
|
||
|
|
|
||
|
|
func (Real) Run(ctx context.Context, s Spec) ([]byte, error) {
|
||
|
|
cmd := exec.CommandContext(ctx, s.Name, s.Args...)
|
||
|
|
if s.Dir != "" {
|
||
|
|
cmd.Dir = s.Dir
|
||
|
|
}
|
||
|
|
if len(s.Env) > 0 {
|
||
|
|
cmd.Env = s.Env
|
||
|
|
}
|
||
|
|
if len(s.Stdin) > 0 {
|
||
|
|
cmd.Stdin = bytesReader(s.Stdin)
|
||
|
|
}
|
||
|
|
out, err := cmd.CombinedOutput()
|
||
|
|
if err != nil {
|
||
|
|
return out, fmt.Errorf("%s %v: %w (output: %s)", s.Name, s.Args, err, string(out))
|
||
|
|
}
|
||
|
|
return out, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
// bytesReader avoids a bytes import in this small file.
|
||
|
|
func bytesReader(b []byte) *bytesBuf { return &bytesBuf{b: b} }
|
||
|
|
|
||
|
|
type bytesBuf struct {
|
||
|
|
b []byte
|
||
|
|
i int
|
||
|
|
}
|
||
|
|
|
||
|
|
func (r *bytesBuf) Read(p []byte) (int, error) {
|
||
|
|
if r.i >= len(r.b) {
|
||
|
|
return 0, eof
|
||
|
|
}
|
||
|
|
n := copy(p, r.b[r.i:])
|
||
|
|
r.i += n
|
||
|
|
return n, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
var eof = fmt.Errorf("EOF")
|
||
|
|
|
||
|
|
// Call is a recorded invocation made against a Fake.
|
||
|
|
type Call struct {
|
||
|
|
Name string
|
||
|
|
Args []string
|
||
|
|
Dir string
|
||
|
|
}
|
||
|
|
|
||
|
|
type fakeResult struct {
|
||
|
|
out []byte
|
||
|
|
err error
|
||
|
|
}
|
||
|
|
|
||
|
|
// Fake records calls and returns configured results.
|
||
|
|
type Fake struct {
|
||
|
|
Calls []Call
|
||
|
|
results map[string]fakeResult
|
||
|
|
}
|
||
|
|
|
||
|
|
// On configures a result for the given name + args. nil args matches any.
|
||
|
|
func (f *Fake) On(name string, args []string) *FakeBuilder {
|
||
|
|
return &FakeBuilder{f: f, name: name, args: args}
|
||
|
|
}
|
||
|
|
|
||
|
|
type FakeBuilder struct {
|
||
|
|
f *Fake
|
||
|
|
name string
|
||
|
|
args []string
|
||
|
|
}
|
||
|
|
|
||
|
|
func (b *FakeBuilder) Return(out []byte, err error) {
|
||
|
|
if b.f.results == nil {
|
||
|
|
b.f.results = map[string]fakeResult{}
|
||
|
|
}
|
||
|
|
b.f.results[fakeKey(b.name, b.args)] = fakeResult{out: out, err: err}
|
||
|
|
}
|
||
|
|
|
||
|
|
func fakeKey(name string, args []string) string {
|
||
|
|
if args == nil {
|
||
|
|
return name + "\x00*"
|
||
|
|
}
|
||
|
|
k := name
|
||
|
|
for _, a := range args {
|
||
|
|
k += "\x00" + a
|
||
|
|
}
|
||
|
|
return k
|
||
|
|
}
|
||
|
|
|
||
|
|
func (f *Fake) Run(ctx context.Context, s Spec) ([]byte, error) {
|
||
|
|
f.Calls = append(f.Calls, Call{Name: s.Name, Args: append([]string(nil), s.Args...), Dir: s.Dir})
|
||
|
|
if r, ok := f.results[fakeKey(s.Name, s.Args)]; ok {
|
||
|
|
return r.out, r.err
|
||
|
|
}
|
||
|
|
if r, ok := f.results[fakeKey(s.Name, nil)]; ok {
|
||
|
|
return r.out, r.err
|
||
|
|
}
|
||
|
|
return nil, fmt.Errorf("fake runner: no result configured for %s %v", s.Name, s.Args)
|
||
|
|
}
|