diff --git a/internal/configuration/sequence.go b/internal/configuration/sequence.go index 6e76064..7ae8937 100644 --- a/internal/configuration/sequence.go +++ b/internal/configuration/sequence.go @@ -11,6 +11,7 @@ import ( "fmt" "os" "regexp" + "strings" "time" "gopkg.in/yaml.v3" @@ -127,10 +128,30 @@ func (sm *SequenceMap) executeSequenceSteps(executor CommandExecutor, seqName st return fmt.Errorf("sequence '%s' failed at step %d: %w", seqName, idx+1, err) } // TODO: provide option to suppress output - tui.SetOutputColor(tui.CBlue, os.Stdout) - enc := yaml.NewEncoder(os.Stdout) - enc.Encode(res) - tui.ResetOutputColor(os.Stdout) + if res != nil { + tui.SetOutputColor(tui.CBlue, os.Stdout) + switch v := res.(type) { + case string: + if len(strings.TrimSpace(v)) > 0 { + if !strings.HasSuffix(v, "\n") { + v += "\n" + } + fmt.Print(v) + } + case fmt.Stringer: + s := v.String() + if len(strings.TrimSpace(s)) > 0 { + if !strings.HasSuffix(s, "\n") { + s += "\n" + } + fmt.Print(s) + } + default: + enc := yaml.NewEncoder(os.Stdout) + enc.Encode(res) + } + tui.ResetOutputColor(os.Stdout) + } } return nil } diff --git a/internal/configuration/sequence_test.go b/internal/configuration/sequence_test.go index 24a7c00..e56a06e 100644 --- a/internal/configuration/sequence_test.go +++ b/internal/configuration/sequence_test.go @@ -418,3 +418,25 @@ func TestExecute_DefaultTimeout_UsedWhenUnset(t *testing.T) { t.Fatalf("unexpected error: %v", err) } } + +// TestExecute_CommandOutputFormatting verifies that string command output is handled +// and that steps returning nil do not print "null". +func TestExecute_CommandOutputFormatting(t *testing.T) { + cmd1 := configuration.SequenceCmd{Cmd: configuration.T("cmd1")} + cmd2 := configuration.SequenceCmd{Cmd: configuration.T("cmd2")} + sm := configuration.SequenceMap{"seq": {cmd1, cmd2}} + + exec := &mockExecutor{ + executeFunc: func(callIdx int, _ context.Context, _ *configuration.SequenceCmd) (any, error) { + if callIdx == 0 { + return "expected command output\n", nil + } + return nil, nil + }, + } + + if err := sm.Execute(exec, "seq"); err != nil { + t.Fatalf("unexpected error executing sequence: %v", err) + } +} + diff --git a/internal/device/ssh/sshdevice.go b/internal/device/ssh/sshdevice.go index ed899e3..42630e3 100644 --- a/internal/device/ssh/sshdevice.go +++ b/internal/device/ssh/sshdevice.go @@ -135,10 +135,11 @@ func (d *SSHDevice) executeCommandString(ctx context.Context, cmd string) (any, case err := <-done: var exitError *stdssh.ExitError if errors.As(err, &exitError) { - return output, fmt.Errorf("exit code (%d)", exitError.ExitStatus()) - } else { + return output.String(), fmt.Errorf("exit code (%d)", exitError.ExitStatus()) + } else if err != nil { return nil, err } + return output.String(), nil case <-ctx.Done(): _ = session.Signal(ssh.SIGKILL) diff --git a/internal/device/ssh/sshdevice_test.go b/internal/device/ssh/sshdevice_test.go index 1df3ccb..28c9c01 100644 --- a/internal/device/ssh/sshdevice_test.go +++ b/internal/device/ssh/sshdevice_test.go @@ -261,13 +261,17 @@ func TestSSHDevice_ExecuteCommand_OutputCaptured(t *testing.T) { defer dev.Close() cmd := mustSequenceCmd(t, "cmd: echo-test") - if _, err := dev.ExecuteCommand(context.Background(), cmd); err != nil { + res, err := dev.ExecuteCommand(context.Background(), cmd) + if err != nil { t.Fatalf("unexpected error executing command: %v", err) } if !strings.Contains(logBuf.String(), want) { t.Errorf("log writer: expected to contain %q, got %q", want, logBuf.String()) } + if outStr, ok := res.(string); !ok || !strings.Contains(outStr, want) { + t.Errorf("ExecuteCommand result: expected string containing %q, got %v", want, res) + } } // TestSSHDevice_ExecuteCommand_ParamsConcatenated verifies that the Cmd field