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) }