Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changes/20251209165547.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
:sparkles: [subprocess] add helpers for starting processes
22 changes: 21 additions & 1 deletion utils/subprocess/executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,21 @@ func ExecuteWithEnvironment(ctx context.Context, loggers logs.Loggers, additiona
return ExecuteWithEnvironmentWithIO(ctx, loggers, nil, additionalEnvVars, messageOnStart, messageOnSuccess, messageOnFailure, cmd, args...)
}

// StartWithEnvironment starts a command (i.e. spawns a subprocess). It allows to specify the environment the subprocess should use. Each entry is of the form "key=value".
func StartWithEnvironment(ctx context.Context, loggers logs.Loggers, additionalEnvVars []string, messageOnStart string, messageOnSuccess, messageOnFailure string, cmd string, args ...string) (*Subprocess, error) {
return StartWithEnvironmentWithIO(ctx, loggers, nil, additionalEnvVars, messageOnStart, messageOnSuccess, messageOnFailure, cmd, args...)
}

// Execute executes a command (i.e. spawns a subprocess).
func Execute(ctx context.Context, loggers logs.Loggers, messageOnStart string, messageOnSuccess, messageOnFailure string, cmd string, args ...string) error {
return ExecuteWithEnvironment(ctx, loggers, nil, messageOnStart, messageOnSuccess, messageOnFailure, cmd, args...)
}

// Start starts a command (i.e. spawns a subprocess).
func Start(ctx context.Context, loggers logs.Loggers, messageOnStart string, messageOnSuccess, messageOnFailure string, cmd string, args ...string) (*Subprocess, error) {
return StartWithEnvironment(ctx, loggers, nil, messageOnStart, messageOnSuccess, messageOnFailure, cmd, args...)
}

// ExecuteAs executes a command (i.e. spawns a subprocess) as a different user.
func ExecuteAs(ctx context.Context, loggers logs.Loggers, messageOnStart string, messageOnSuccess, messageOnFailure string, as *commandUtils.CommandAsDifferentUser, cmd string, args ...string) error {
return ExecuteAsWithEnvironment(ctx, loggers, nil, messageOnStart, messageOnSuccess, messageOnFailure, as, cmd, args...)
Expand All @@ -86,7 +96,7 @@ func ExecuteWithSudo(ctx context.Context, loggers logs.Loggers, messageOnStart s
return ExecuteAs(ctx, loggers, messageOnStart, messageOnSuccess, messageOnFailure, commandUtils.Sudo(), cmd, args...)
}

// ExecuteWithEnvironment executes a command (i.e. spawns a subprocess) with overridden stdin/stdout/stderr. It allows to specify the environment the subprocess should use. Each entry is of the form "key=value".
// ExecuteWithEnvironmentWithIO executes a command (i.e. spawns a subprocess) with overridden stdin/stdout/stderr. It allows to specify the environment the subprocess should use. Each entry is of the form "key=value".
func ExecuteWithEnvironmentWithIO(ctx context.Context, loggers logs.Loggers, io ICommandIO, additionalEnvVars []string, messageOnStart string, messageOnSuccess, messageOnFailure string, cmd string, args ...string) (err error) {
p, err := NewWithEnvironmentWithIO(ctx, loggers, io, additionalEnvVars, messageOnStart, messageOnSuccess, messageOnFailure, cmd, args...)
if err != nil {
Expand All @@ -95,6 +105,16 @@ func ExecuteWithEnvironmentWithIO(ctx context.Context, loggers logs.Loggers, io
return p.Execute()
}

// StartWithEnvironmentWithIO starts a command (i.e. spawns a subprocess) with overridden stdin/stdout/stderr. It allows to specify the environment the subprocess should use. Each entry is of the form "key=value".
func StartWithEnvironmentWithIO(ctx context.Context, loggers logs.Loggers, io ICommandIO, additionalEnvVars []string, messageOnStart string, messageOnSuccess, messageOnFailure string, cmd string, args ...string) (p *Subprocess, err error) {
p, err = NewWithEnvironmentWithIO(ctx, loggers, io, additionalEnvVars, messageOnStart, messageOnSuccess, messageOnFailure, cmd, args...)
if err != nil {
return
}
err = p.Start()
return
}

// ExecuteWithIO executes a command (i.e. spawns a subprocess) with overridden stdin/stdout/stderr.
func ExecuteWithIO(ctx context.Context, loggers logs.Loggers, io ICommandIO, messageOnStart string, messageOnSuccess, messageOnFailure string, cmd string, args ...string) error {
return ExecuteWithEnvironmentWithIO(ctx, loggers, io, nil, messageOnStart, messageOnSuccess, messageOnFailure, cmd, args...)
Expand Down
92 changes: 92 additions & 0 deletions utils/subprocess/executor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ func (t *testIO) Register(context.Context) (io.Reader, io.Writer, io.Writer) {
}

type execFunc func(ctx context.Context, l logs.Loggers, cmd string, args ...string) error
type startFunc func(ctx context.Context, l logs.Loggers, cmd string, args ...string) (*Subprocess, error)

func newDefaultExecutor(t *testing.T) execFunc {
t.Helper()
Expand All @@ -68,6 +69,26 @@ func newCustomIOExecutor(t *testing.T, customIO *testIO) execFunc {
}
}

func newDefaultStarter(t *testing.T) startFunc {
t.Helper()
return func(ctx context.Context, l logs.Loggers, cmd string, args ...string) (*Subprocess, error) {
return Start(ctx, l, "", "", "", cmd, args...)
}
}

func newCustomIOStarter(t *testing.T, customIO *testIO) startFunc {
t.Helper()
return func(ctx context.Context, l logs.Loggers, cmd string, args ...string) (p *Subprocess, err error) {
p = &Subprocess{}
err = p.SetupWithEnvironmentWithCustomIO(ctx, l, customIO, nil, "", "", "", cmd, args...)
if err != nil {
return
}
err = p.Start()
return
}
}

func TestExecuteEmptyLines(t *testing.T) {
t.Skip("would need to be reinstated when fixed")
defer goleak.VerifyNone(t)
Expand Down Expand Up @@ -375,6 +396,77 @@ func TestExecute(t *testing.T) {
}
}

func TestStart(t *testing.T) {
currentDir, err := os.Getwd()
require.NoError(t, err)

tests := []struct {
name string
cmdWindows string
argWindows []string
cmdOther string
argOther []string
expectIO bool
}{
{
name: "ShortProcess",
cmdWindows: "cmd",
argWindows: []string{"/c", "dir", currentDir},
cmdOther: "ls",
argOther: []string{"-l", currentDir},
expectIO: true,
},
{
name: "LongProcess",
cmdWindows: "cmd",
argWindows: []string{"/c", fmt.Sprintf("ping -n 2 -w %v localhost > nul", time.Second.Milliseconds())}, // See https://stackoverflow.com/a/79268314/45375
cmdOther: "sleep",
argOther: []string{"1"},
expectIO: false,
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
// defer goleak.VerifyNone(t)

customIO := newTestIO()
executors := []struct {
name string
start startFunc
io *testIO
}{
{"normal", newDefaultStarter(t), nil},
{"with IO", newCustomIOStarter(t, customIO), customIO},
}

for _, executor := range executors {
t.Run(executor.name, func(t *testing.T) {
var loggers logs.Loggers = &logs.GenericLoggers{}
err := loggers.Check()
assert.Error(t, err)

_, err = executor.start(context.Background(), loggers, "ls")
assert.Error(t, err)

loggers, err = logs.NewLogrLogger(logstest.NewTestLogger(t), "test")
require.NoError(t, err)

var p *Subprocess
if platform.IsWindows() {
p, err = executor.start(context.Background(), loggers, test.cmdWindows, test.argWindows...)
} else {
p, err = executor.start(context.Background(), loggers, test.cmdOther, test.argOther...)
}
require.NoError(t, err)
require.NotNil(t, p)
require.NoError(t, p.Wait(context.Background()))
})
}
})
}
}

func TestExecuteWithCustomIO_Stderr(t *testing.T) {
if platform.IsWindows() {
t.Skip("Uses bash and redirection so can't run on Windows")
Expand Down
Loading