This commit is contained in:
+44
-74
@@ -7,7 +7,6 @@ import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
@@ -28,37 +27,51 @@ import (
|
||||
const cmdTimeout = 60 * time.Second
|
||||
|
||||
// CommandRunner allows overriding the command execution function for testing.
|
||||
// In tests, register a handler with SetMock; the runner translates the
|
||||
// MockCommand into a /bin/sh script that reproduces the stdout/stderr/exit
|
||||
// behavior — no helper process, no temp file.
|
||||
var CommandRunner = func(ctx context.Context, name string, args ...string) *exec.Cmd {
|
||||
mockMu.Lock()
|
||||
handler, ok := mockCmds[name]
|
||||
mockMu.Unlock()
|
||||
|
||||
if ok {
|
||||
mockRes := handler(args)
|
||||
tmpFile, err := os.CreateTemp("", "nadir-mock-*.json")
|
||||
if err != nil {
|
||||
log.Printf("oscmd mock: failed to create temp file: %v", err)
|
||||
return exec.CommandContext(ctx, name, args...)
|
||||
}
|
||||
|
||||
encoder := json.NewEncoder(tmpFile)
|
||||
if err := encoder.Encode(mockRes); err != nil {
|
||||
log.Printf("oscmd mock: failed to write json: %v", err)
|
||||
tmpFile.Close()
|
||||
return exec.CommandContext(ctx, name, args...)
|
||||
}
|
||||
tmpPath := tmpFile.Name()
|
||||
tmpFile.Close()
|
||||
|
||||
cmd := exec.CommandContext(ctx, os.Args[0])
|
||||
cmd.Env = append(os.Environ(),
|
||||
"GO_WANT_HELPER_PROCESS=1",
|
||||
"NADIR_MOCK_FILE="+tmpPath,
|
||||
)
|
||||
return cmd
|
||||
if !ok {
|
||||
return exec.CommandContext(ctx, name, args...)
|
||||
}
|
||||
return exec.CommandContext(ctx, "/bin/sh", "-c", mockScript(handler(args)))
|
||||
}
|
||||
|
||||
return exec.CommandContext(ctx, name, args...)
|
||||
// mockScript builds the shell script that emits a MockCommand's behavior.
|
||||
// Uses printf so backslashes and percent signs in output pass through verbatim.
|
||||
func mockScript(m MockCommand) string {
|
||||
var b strings.Builder
|
||||
if len(m.Lines) > 0 {
|
||||
for _, line := range m.Lines {
|
||||
b.WriteString("printf '%s\\n' ")
|
||||
b.WriteString(shellQuote(line))
|
||||
b.WriteByte('\n')
|
||||
if m.DelayMs > 0 {
|
||||
fmt.Fprintf(&b, "sleep %g\n", float64(m.DelayMs)/1000)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if m.Stdout != "" {
|
||||
b.WriteString("printf '%s' ")
|
||||
b.WriteString(shellQuote(m.Stdout))
|
||||
b.WriteByte('\n')
|
||||
}
|
||||
if m.Stderr != "" {
|
||||
b.WriteString("printf '%s' ")
|
||||
b.WriteString(shellQuote(m.Stderr))
|
||||
b.WriteString(" 1>&2\n")
|
||||
}
|
||||
}
|
||||
fmt.Fprintf(&b, "exit %d\n", m.ExitCode)
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// shellQuote returns s wrapped in single quotes, with embedded single quotes escaped.
|
||||
func shellQuote(s string) string {
|
||||
return "'" + strings.ReplaceAll(s, "'", `'\''`) + "'"
|
||||
}
|
||||
|
||||
// Run executes name with args and returns trimmed stdout. On failure it wraps
|
||||
@@ -283,11 +296,11 @@ func OK() *StatusOutput {
|
||||
|
||||
// MockCommand holds the behavior for a mocked command.
|
||||
type MockCommand struct {
|
||||
Stdout string `json:"stdout"`
|
||||
Stderr string `json:"stderr"`
|
||||
ExitCode int `json:"exit_code"`
|
||||
Lines []string `json:"lines,omitempty"`
|
||||
DelayMs int `json:"delay_ms,omitempty"`
|
||||
Stdout string
|
||||
Stderr string
|
||||
ExitCode int
|
||||
Lines []string
|
||||
DelayMs int
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -309,46 +322,3 @@ func ClearMocks() {
|
||||
clear(mockCmds)
|
||||
}
|
||||
|
||||
// RunHelperProcess executes the mock helper process logic if GO_WANT_HELPER_PROCESS is set.
|
||||
// It returns true if it ran (and exits the process), false otherwise.
|
||||
func RunHelperProcess() bool {
|
||||
if os.Getenv("GO_WANT_HELPER_PROCESS") != "1" {
|
||||
return false
|
||||
}
|
||||
mockFile := os.Getenv("NADIR_MOCK_FILE")
|
||||
if mockFile == "" {
|
||||
os.Exit(1)
|
||||
}
|
||||
defer os.Remove(mockFile)
|
||||
|
||||
data, err := os.ReadFile(mockFile)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "mock helper: read failed: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
var mock MockCommand
|
||||
if err := json.Unmarshal(data, &mock); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "mock helper: unmarshal failed: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if len(mock.Lines) > 0 {
|
||||
for _, line := range mock.Lines {
|
||||
fmt.Fprintln(os.Stdout, line)
|
||||
if mock.DelayMs > 0 {
|
||||
time.Sleep(time.Duration(mock.DelayMs) * time.Millisecond)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if mock.Stdout != "" {
|
||||
fmt.Fprint(os.Stdout, mock.Stdout)
|
||||
}
|
||||
if mock.Stderr != "" {
|
||||
fmt.Fprint(os.Stderr, mock.Stderr)
|
||||
}
|
||||
}
|
||||
|
||||
os.Exit(mock.ExitCode)
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -1,19 +1,11 @@
|
||||
package oscmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
if RunHelperProcess() {
|
||||
return
|
||||
}
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
func TestRunTrimsStdout(t *testing.T) {
|
||||
out, err := Run("echo", "hello")
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user