*
* Copyright (c) 2025 Bocloud Technologies Co., Ltd.
* installer is licensed under Mulan PSL v2.
* You can use this software according to the terms and conditions of the Mulan PSL v2.
* You may obtain a copy of Mulan PSL v2 at:
* <http://license.coscl.org.cn/MulanPSL2>
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
* See the Mulan PSL v2 for more details.
*
*/
package exec
import (
"bytes"
"errors"
"io"
"os"
"os/exec"
"strings"
"testing"
"time"
"github.com/agiledragon/gomonkey/v2"
"github.com/stretchr/testify/assert"
"gopkg.openfuyao.cn/bkeadm/utils/log"
)
const (
testShortTimeout = 1 * time.Second
testMediumTimeout = 5 * time.Second
)
func TestCommandExecutorStruct(t *testing.T) {
executor := &CommandExecutor{}
assert.NotNil(t, executor)
}
func TestCommandResultStruct(t *testing.T) {
result := &CommandResult{}
result.Cmd = &exec.Cmd{}
result.Stdout = nil
result.Stderr = nil
assert.NotNil(t, result.Cmd)
assert.Nil(t, result.Stdout)
assert.Nil(t, result.Stderr)
}
func TestCommandTimeoutContextStruct(t *testing.T) {
timer := time.NewTimer(testShortTimeout)
defer timer.Stop()
buffer := &bytes.Buffer{}
cmd := &exec.Cmd{}
ctx := &commandTimeoutContext{
cmd: cmd,
done: make(chan error, 1),
timer: timer,
timeout: testShortTimeout,
command: "test-command",
buffer: buffer,
}
assert.Equal(t, cmd, ctx.cmd)
assert.NotNil(t, ctx.done)
assert.Equal(t, timer, ctx.timer)
assert.Equal(t, testShortTimeout, ctx.timeout)
assert.Equal(t, "test-command", ctx.command)
assert.Equal(t, buffer, ctx.buffer)
}
func TestExecuteCommand(t *testing.T) {
executor := &CommandExecutor{}
patches := gomonkey.NewPatches()
defer patches.Reset()
patches.ApplyFunc((*CommandExecutor).ExecuteCommandWithEnv, func(c *CommandExecutor, env []string, command string, arg ...string) error {
assert.Equal(t, []string{}, env)
assert.Equal(t, "echo", command)
assert.Equal(t, []string{"hello"}, arg)
return nil
})
err := executor.ExecuteCommand("echo", "hello")
assert.NoError(t, err)
}
func TestExecuteCommandWithEnv(t *testing.T) {
executor := &CommandExecutor{}
patches := gomonkey.NewPatches()
defer patches.Reset()
patches.ApplyFunc(startCommand, func(env []string, command string, arg ...string) (*CommandResult, error) {
assert.Equal(t, []string{"ENV_VAR=value"}, env)
assert.Equal(t, "echo", command)
assert.Equal(t, []string{"hello"}, arg)
return &CommandResult{
Cmd: &exec.Cmd{},
Stdout: io.NopCloser(strings.NewReader("hello")),
Stderr: io.NopCloser(strings.NewReader("")),
}, nil
})
patches.ApplyFunc(logOutput, func(stdout, stderr io.ReadCloser) {})
err := executor.ExecuteCommandWithEnv([]string{"ENV_VAR=value"}, "echo", "hello")
assert.Error(t, err)
}
func TestExecuteCommandWithOutput(t *testing.T) {
executor := &CommandExecutor{}
patches := gomonkey.NewPatches()
defer patches.Reset()
patches.ApplyFunc(logCommand, func(command string, arg ...string) {})
patches.ApplyFunc(runCommandWithOutput, func(cmd *exec.Cmd, combinedOutput bool) (string, error) {
return "command output", nil
})
output, err := executor.ExecuteCommandWithOutput("echo", "hello")
assert.NoError(t, err)
assert.Equal(t, "command output", output)
}
func TestExecuteCommandWithCombinedOutput(t *testing.T) {
executor := &CommandExecutor{}
patches := gomonkey.NewPatches()
defer patches.Reset()
patches.ApplyFunc(logCommand, func(command string, arg ...string) {})
patches.ApplyFunc(runCommandWithOutput, func(cmd *exec.Cmd, combinedOutput bool) (string, error) {
assert.True(t, combinedOutput)
return "combined output", nil
})
output, err := executor.ExecuteCommandWithCombinedOutput("echo", "hello")
assert.NoError(t, err)
assert.Equal(t, "combined output", output)
}
func TestExecuteCommandWithTimeout(t *testing.T) {
executor := &CommandExecutor{}
patches := gomonkey.NewPatches()
defer patches.Reset()
patches.ApplyFunc(waitForCommandWithTimeout, func(ctx commandTimeoutContext) (string, error) {
return "timeout output", nil
})
output, err := executor.ExecuteCommandWithTimeout(testShortTimeout, "echo", "hello")
assert.NoError(t, err)
assert.Equal(t, "timeout output", output)
}
func TestExecuteCommandResidentBinary(t *testing.T) {
executor := &CommandExecutor{}
patches := gomonkey.NewPatches()
defer patches.Reset()
patches.ApplyFunc(time.Sleep, func(d time.Duration) {})
patches.ApplyFunc((*exec.Cmd).Run, func(cmd *exec.Cmd) error {
return nil
})
err := executor.ExecuteCommandResidentBinary(time.Millisecond, "sleep", "1")
assert.NoError(t, err)
}
func TestStartCommand(t *testing.T) {
env := []string{"TEST_VAR=test_value"}
command := "echo"
args := []string{"hello"}
patches := gomonkey.NewPatches()
defer patches.Reset()
patches.ApplyFunc(logCommand, func(cmd string, args ...string) {})
patches.ApplyFunc(exec.Command, func(name string, arg ...string) *exec.Cmd {
assert.Equal(t, "echo", name)
assert.Equal(t, []string{"hello"}, arg)
return &exec.Cmd{}
})
patches.ApplyFunc((*exec.Cmd).StdoutPipe, func(cmd *exec.Cmd) (io.ReadCloser, error) {
return io.NopCloser(strings.NewReader("hello")), nil
})
patches.ApplyFunc((*exec.Cmd).StderrPipe, func(cmd *exec.Cmd) (io.ReadCloser, error) {
return io.NopCloser(strings.NewReader("")), nil
})
patches.ApplyFunc((*exec.Cmd).Start, func(cmd *exec.Cmd) error {
return nil
})
result, err := startCommand(env, command, args...)
assert.NoError(t, err)
assert.NotNil(t, result)
assert.Equal(t, env, result.Cmd.Env)
}
func TestStartCommandError(t *testing.T) {
env := []string{}
command := "invalid-command"
args := []string{}
patches := gomonkey.NewPatches()
defer patches.Reset()
patches.ApplyFunc(logCommand, func(cmd string, args ...string) {})
patches.ApplyFunc(exec.Command, func(name string, arg ...string) *exec.Cmd {
return &exec.Cmd{}
})
patches.ApplyFunc((*exec.Cmd).Start, func(cmd *exec.Cmd) error {
return errors.New("command failed to start")
})
result, err := startCommand(env, command, args...)
assert.Error(t, err)
assert.Nil(t, result)
}
func TestLogFromReader(t *testing.T) {
reader := io.NopCloser(strings.NewReader("line1\nline2\nline3"))
var capturedLog string
logFromReader(reader)
assert.Equal(t, "", capturedLog)
}
func TestLogOutput(t *testing.T) {
stdout := io.NopCloser(strings.NewReader("stdout content"))
stderr := io.NopCloser(strings.NewReader("stderr content"))
patches := gomonkey.NewPatches()
defer patches.Reset()
patches.ApplyFunc(logFromReader, func(reader io.ReadCloser) {})
logOutput(stdout, stderr)
assert.True(t, true)
}
func TestLogOutputNil(t *testing.T) {
patches := gomonkey.NewPatches()
defer patches.Reset()
patches.ApplyFunc(log.Warnf, func(format string, args ...interface{}) {})
logOutput(nil, nil)
assert.True(t, true)
}
func TestRunCommandWithOutput(t *testing.T) {
cmd := exec.Command("echo", "hello")
patches := gomonkey.NewPatches()
defer patches.Reset()
patches.ApplyFunc((*exec.Cmd).Output, func(c *exec.Cmd) ([]byte, error) {
return []byte("hello\n"), nil
})
output, err := runCommandWithOutput(cmd, false)
assert.NoError(t, err)
assert.Equal(t, "hello", output)
}
func TestRunCommandWithOutputError(t *testing.T) {
cmd := exec.Command("invalid-command")
patches := gomonkey.NewPatches()
defer patches.Reset()
patches.ApplyFunc((*exec.Cmd).Output, func(c *exec.Cmd) ([]byte, error) {
return []byte(""), errors.New("command failed")
})
patches.ApplyFunc(assertErrorType, func(err error) string {
return "assertion error"
})
output, err := runCommandWithOutput(cmd, false)
assert.Error(t, err)
assert.Contains(t, output, "")
}
func TestRunCommandWithCombinedOutput(t *testing.T) {
cmd := exec.Command("echo", "hello")
patches := gomonkey.NewPatches()
defer patches.Reset()
patches.ApplyFunc((*exec.Cmd).CombinedOutput, func(c *exec.Cmd) ([]byte, error) {
return []byte("hello\n"), nil
})
output, err := runCommandWithOutput(cmd, true)
assert.NoError(t, err)
assert.Equal(t, "hello", output)
}
func TestAssertErrorType(t *testing.T) {
exitErr := &exec.ExitError{
Stderr: []byte("exit error message"),
}
result := assertErrorType(exitErr)
assert.Equal(t, "exit error message", result)
execErr := &exec.Error{
Err: errors.New("exec error"),
}
result = assertErrorType(execErr)
assert.Equal(t, "exec: \"\": exec error", result)
otherErr := errors.New("other error")
result = assertErrorType(otherErr)
assert.Equal(t, "", result)
}
func TestHandleTimeout(t *testing.T) {
cmd := &exec.Cmd{}
cmd.Process = &os.Process{}
timer := time.NewTimer(testShortTimeout)
defer timer.Stop()
patches := gomonkey.NewPatches()
defer patches.Reset()
patches.ApplyFunc((*os.Process).Kill, func(p *os.Process) error {
return nil
})
output, err, interruptSent := handleTimeout(cmd, "test-command", true, timer, testShortTimeout)
assert.Equal(t, "", output)
assert.NotNil(t, err)
assert.True(t, interruptSent)
patches.ApplyFunc((*os.Process).Signal, func(p *os.Process) error {
return nil
})
output, err, interruptSent = handleTimeout(cmd, "test-command", false, timer, testShortTimeout)
assert.Equal(t, "", output)
assert.Nil(t, err)
assert.True(t, interruptSent)
}
func TestHandleKillProcess(t *testing.T) {
cmd := exec.Command("sleep", "10")
err := cmd.Start()
assert.NoError(t, err)
patches := gomonkey.NewPatches()
defer patches.Reset()
patches.ApplyFunc(log.Infof, func(format string, args ...interface{}) {})
patches.ApplyFunc(log.Errorf, func(format string, args ...interface{}) {})
patches.ApplyFunc((*os.Process).Kill, func(p *os.Process) error {
return nil
})
output, err := handleKillProcess(cmd, "sleep")
assert.Equal(t, "", output)
assert.Error(t, err)
}
func TestHandleCommandDone(t *testing.T) {
buffer := &bytes.Buffer{}
buffer.WriteString("test output")
output, err := handleCommandDone(nil, false, "test-command", buffer)
assert.Equal(t, "test output", output)
assert.NoError(t, err)
output, err = handleCommandDone(errors.New("command failed"), false, "test-command", buffer)
assert.Equal(t, "test output", output)
assert.Error(t, err)
output, err = handleCommandDone(nil, true, "test-command", buffer)
assert.Equal(t, "test output", output)
assert.Error(t, err)
assert.Contains(t, err.Error(), "timeout waiting for the command")
}
func TestCommandWithOutputFileInternal(t *testing.T) {
command := "echo"
outfileArg := "> "
args := []string{"hello"}
output, _ := commandWithOutputFileInternal(command, outfileArg, nil, args...)
assert.NotNil(t, output)
}
func TestCommandWithOutputFileInternalTimeout(t *testing.T) {
timeout := time.Millisecond * 100
command := "sleep"
outfileArg := ""
args := []string{"1"}
output, err := commandWithOutputFileInternal(command, outfileArg, &timeout, args...)
assert.NotNil(t, output)
assert.NotNil(t, err)
}
func TestExecutorInterface(t *testing.T) {
var executor Executor = &CommandExecutor{}
_ = executor.ExecuteCommand
_ = executor.ExecuteCommandWithEnv
_ = executor.ExecuteCommandWithOutput
_ = executor.ExecuteCommandWithCombinedOutput
_ = executor.ExecuteCommandWithOutputFile
_ = executor.ExecuteCommandWithOutputFileTimeout
_ = executor.ExecuteCommandWithTimeout
_ = executor.ExecuteCommandResidentBinary
assert.True(t, true)
}
func TestCommandExecutorImplementsExecutor(t *testing.T) {
var _ Executor = &CommandExecutor{}
assert.True(t, true)
}