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 } }