func/test/common/iteractivecmd.go

148 lines
3.5 KiB
Go

package common
import (
"bytes"
"fmt"
"io"
"os"
"os/exec"
"strings"
"testing"
"time"
"github.com/Netflix/go-expect"
"github.com/creack/pty"
"github.com/hinshun/vt10x"
)
type TestInteractiveCmd struct {
TestCmd *TestExecCmd
T *testing.T
// Sleep interval before first command input
StartSleepInterval time.Duration
// Sleep interval between each subcommand
CommandSleepInterval time.Duration
// Sleep interval after last command completion.
// Required time to give to process to complete before EOF
CompletionSleepInterval time.Duration
// Timeout before kill the cmd in case of some failure
CompletionTimeout time.Duration
}
func NewTestShellInteractiveCmd(t *testing.T) *TestInteractiveCmd {
testShell := NewKnFuncShellCli(t)
return &TestInteractiveCmd{
TestCmd: testShell,
T: t,
StartSleepInterval: time.Second * 2,
CommandSleepInterval: time.Second * 1,
CompletionSleepInterval: time.Second * 2,
CompletionTimeout: time.Second * 15,
}
}
// PrepareRun creates a go function used to start kn func (binary) that requires user interaction such as `func config command`
func (f *TestInteractiveCmd) PrepareRun(funcCommand ...string) func(args ...string) TestExecCmdResult {
return func(userInput ...string) TestExecCmdResult {
// Prepare Command args
finalArgs := f.TestCmd.BinaryArgs
if finalArgs == nil {
finalArgs = funcCommand
} else if funcCommand != nil {
finalArgs = append(finalArgs, funcCommand...)
}
if f.TestCmd.ShouldDumpCmdLine {
f.T.Log(f.TestCmd.Binary, strings.Join(finalArgs, " "))
}
// Prepare terminal emulator
ptm, pts, err := pty.Open()
if err != nil {
f.T.Fatal(err)
}
term := vt10x.New(vt10x.WithWriter(pts))
c, err := expect.NewConsole(expect.WithStdin(ptm), expect.WithStdout(term), expect.WithCloser(ptm, pts))
if err != nil {
f.T.Fatal(err)
}
f.T.Cleanup(func() { c.Close() })
// Prepare and start command on terminal emulator
var stdout bytes.Buffer
cmd := exec.Command(f.TestCmd.Binary, finalArgs...)
cmd.Stdin = c.Tty()
cmd.Stdout = io.MultiWriter(c.Tty(), &stdout)
cmd.Stderr = io.MultiWriter(c.Tty(), &stdout)
if f.TestCmd.SourceDir != "" {
cmd.Dir = f.TestCmd.SourceDir
}
cmd.Env = append(os.Environ(), f.TestCmd.Env...)
err = cmd.Start()
if err != nil {
f.T.Fatalf("error on start command: %v\n", err)
}
// Monitor kn func command completion
doneCh := make(chan error, 1)
go func() {
_, err := c.ExpectEOF()
doneCh <- err
}()
// Input user entries on Terminal
for i, subcmd := range userInput {
if i == 0 {
time.Sleep(f.StartSleepInterval)
} else {
time.Sleep(f.CommandSleepInterval)
}
_, err = c.Send(subcmd)
if err != nil {
f.T.Logf("error sending user input comand to console: %v\n", err)
}
}
time.Sleep(f.CompletionSleepInterval)
err = c.Tty().Close()
if err != nil {
f.T.Logf("error on TTY close: %v\n", err)
}
// Wait Command Completion
select {
case err = <-doneCh:
if err != nil {
fmt.Printf("process completed with error: %v\n", err)
}
case <-time.After(f.CompletionTimeout):
err = cmd.Process.Kill()
if err != nil {
fmt.Printf("error killing process after timeout: %v\n", err)
} else {
err = fmt.Errorf("timeout occurred")
}
}
// Collect results
result := TestExecCmdResult{
Out: stdout.String(),
Error: err,
}
if err == nil && f.TestCmd.ShouldDumpOnSuccess {
f.T.Log(result.Out)
}
if err != nil {
f.T.Log(result.Out)
f.T.Log(err.Error())
}
return result
}
}