Files

107 lines
2.9 KiB
Go

// SPDX-License-Identifier: MIT
// Package execx provides a thin, testable abstraction over external command
// execution.
//
// All privileged work in prx goes through a Runner so that the orchestration
// logic can be unit tested without touching the real system.
package execx
import (
"bytes"
"context"
"errors"
"fmt"
"os/exec"
"strings"
)
// Result is the outcome of running an external command.
type Result struct {
Stdout []byte
Stderr []byte
Code int
}
// Runner abstracts external command execution.
type Runner interface {
// Run executes name with args. If stdin is non-nil it is fed to the
// process standard input. A non-nil error is returned when the command
// could not be started or exited with a non-zero status (*ExitError).
Run(ctx context.Context, stdin []byte, name string, args ...string) (Result, error)
// LookPath resolves the absolute path of an executable.
LookPath(name string) (string, error)
}
// ExitError indicates that a command started but exited with a non-zero code.
type ExitError struct {
Name string
Args []string
Code int
Stderr []byte
}
func (e *ExitError) Error() string {
msg := strings.TrimSpace(string(e.Stderr))
if msg == "" {
msg = "no stderr"
}
return fmt.Sprintf("%s %s: exit status %d: %s", e.Name, strings.Join(e.Args, " "), e.Code, msg)
}
// OSRunner runs commands on the host.
type OSRunner struct{}
// Run implements Runner.
func (OSRunner) Run(ctx context.Context, stdin []byte, name string, args ...string) (Result, error) {
cmd := exec.CommandContext(ctx, name, args...)
if stdin != nil {
cmd.Stdin = bytes.NewReader(stdin)
}
var out, errb bytes.Buffer
cmd.Stdout = &out
cmd.Stderr = &errb
err := cmd.Run()
res := Result{Stdout: out.Bytes(), Stderr: errb.Bytes()}
if err != nil {
var ee *exec.ExitError
if errors.As(err, &ee) {
res.Code = ee.ExitCode()
return res, &ExitError{Name: name, Args: args, Code: res.Code, Stderr: res.Stderr}
}
return res, err
}
return res, nil
}
// LookPath implements Runner.
func (OSRunner) LookPath(name string) (string, error) { return exec.LookPath(name) }
// Call records a single fake invocation.
type Call struct {
Name string
Args []string
Stdin []byte
}
// Fake is an in-memory Runner for tests.
type Fake struct {
// Handler produces the result for a call. When nil an empty successful
// result is returned.
Handler func(name string, args []string, stdin []byte) (Result, error)
// Calls records every invocation in order.
Calls []Call
}
// Run implements Runner.
func (f *Fake) Run(_ context.Context, stdin []byte, name string, args ...string) (Result, error) {
f.Calls = append(f.Calls, Call{Name: name, Args: append([]string(nil), args...), Stdin: append([]byte(nil), stdin...)})
if f.Handler == nil {
return Result{}, nil
}
return f.Handler(name, args, stdin)
}
// LookPath implements Runner.
func (f *Fake) LookPath(name string) (string, error) { return "/usr/bin/" + name, nil }