// Package tests provides TUI test driver for SSH testing package tests import ( "bytes" "fmt" "io" "time" "golang.org/x/crypto/ssh" ) // TUIDriver provides a driver for interacting with the TUI over SSH type TUIDriver struct { session *ssh.Session stdin io.WriteCloser stdout io.Reader stderr io.Reader outputBuf *bytes.Buffer width int height int } // NewTUIDriver creates a new TUI driver connected via SSH func NewTUIDriver(session *ssh.Session, width, height int) (*TUIDriver, error) { stdin, err := session.StdinPipe() if err != nil { return nil, fmt.Errorf("failed to get stdin pipe: %w", err) } stdout, err := session.StdoutPipe() if err != nil { return nil, fmt.Errorf("failed to get stdout pipe: %w", err) } stderr, err := session.StderrPipe() if err != nil { return nil, fmt.Errorf("failed to get stderr pipe: %w", err) } driver := &TUIDriver{ session: session, stdin: stdin, stdout: stdout, stderr: stderr, outputBuf: &bytes.Buffer{}, width: width, height: height, } return driver, nil } // SendKey sends a single key to the TUI func (d *TUIDriver) SendKey(key byte) error { _, err := d.stdin.Write([]byte{key}) return err } // SendKeys sends multiple keys to the TUI func (d *TUIDriver) SendKeys(keys string) error { _, err := d.stdin.Write([]byte(keys)) return err } // SendEscape sends the Escape key func (d *TUIDriver) SendEscape() error { return d.SendKey(0x1b) // ESC } // SendEnter sends the Enter key func (d *TUIDriver) SendEnter() error { return d.SendKey('\n') } // SendCtrlC sends Ctrl+C (SIGINT) func (d *TUIDriver) SendCtrlC() error { return d.SendKey(0x03) // ETX (Ctrl+C) } // SendArrowUp sends the up arrow key func (d *TUIDriver) SendArrowUp() error { _, err := d.stdin.Write([]byte{0x1b, '[', 'A'}) return err } // SendArrowDown sends the down arrow key func (d *TUIDriver) SendArrowDown() error { _, err := d.stdin.Write([]byte{0x1b, '[', 'B'}) return err } // SendArrowLeft sends the left arrow key func (d *TUIDriver) SendArrowLeft() error { _, err := d.stdin.Write([]byte{0x1b, '[', 'D'}) return err } // SendArrowRight sends the right arrow key func (d *TUIDriver) SendArrowRight() error { _, err := d.stdin.Write([]byte{0x1b, '[', 'C'}) return err } // SendTab sends the Tab key func (d *TUIDriver) SendTab() error { return d.SendKey('\t') } // CaptureScreen reads the current screen output func (d *TUIDriver) CaptureScreen(timeout time.Duration) (string, error) { // Clear previous buffer d.outputBuf.Reset() // Read with timeout done := make(chan error, 1) go func() { buf := make([]byte, 4096) for { n, err := d.stdout.Read(buf) if n > 0 { d.outputBuf.Write(buf[:n]) } if err != nil { done <- err return } } }() select { case <-time.After(timeout): return d.outputBuf.String(), nil case err := <-done: if err == io.EOF { return d.outputBuf.String(), nil } return d.outputBuf.String(), err } } // WaitForOutput waits for specific output to appear func (d *TUIDriver) WaitForOutput(expected string, timeout time.Duration) error { deadline := time.Now().Add(timeout) d.outputBuf.Reset() buf := make([]byte, 1024) for time.Now().Before(deadline) { n, err := d.stdout.Read(buf) if n > 0 { d.outputBuf.Write(buf[:n]) if bytes.Contains(d.outputBuf.Bytes(), []byte(expected)) { return nil } } if err != nil && err != io.EOF { // Continue on timeout time.Sleep(50 * time.Millisecond) continue } // Short sleep to prevent tight loop time.Sleep(50 * time.Millisecond) } return fmt.Errorf("timeout waiting for output containing %q. Got: %s", expected, d.outputBuf.String()) } // Close closes the TUI driver and session func (d *TUIDriver) Close() error { d.stdin.Close() return d.session.Close() } // Wait waits for the session to complete func (d *TUIDriver) Wait() error { return d.session.Wait() } // Signal sends a signal to the session func (d *TUIDriver) Signal(sig ssh.Signal) error { return d.session.Signal(sig) }