wails-release/internal/runner/runner.go
Leon Mika 2350046601 Add Runner abstraction with Real and Fake implementations
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 09:45:26 +10:00

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