Files
coder/pty/ptytest/ptytest.go
T
Spike Curtis ee4126e913 test: refactor CLI create tests not to use PTY (#25807)
<!--

If you have used AI to produce some or all of this PR, please ensure you have read our [AI Contribution guidelines](https://coder.com/docs/about/contributing/AI_CONTRIBUTING) before submitting.

-->Part of https://github.com/coder/internal/issues/1400  
  
Refactors CLI tests of the `create` command as the first batch of tests refactored to take a PTY out of the loop.

One interesting difference I noticed between PTY and a direct pipe to standard in is that on the PTY we write `\r` to enter some input, but the kernel actually sends `\n` (or maybe `\r\n`) to the process, at least on Unix. (On windows we sent `\r\n` into the PTY).  This is reflected in the implementation of the `Writer` , otherwise mostly inspired by the PTYTest equivalents.
2026-05-28 17:50:37 -04:00

136 lines
2.6 KiB
Go

package ptytest
import (
"runtime"
"sync"
"testing"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/pty"
"github.com/coder/coder/v2/testutil/expecter"
"github.com/coder/serpent"
)
func New(t *testing.T, opts ...pty.Option) *PTY {
t.Helper()
ptty, err := newTestPTY(opts...)
require.NoError(t, err)
e := expecter.New(t, ptty.Output(), "cmd")
r := &PTY{
t: t,
Expecter: *e,
PTY: ptty,
}
// Ensure pty is cleaned up at the end of test.
t.Cleanup(func() {
_ = r.Close()
})
return r
}
// Start starts a new process asynchronously and returns a PTYCmd and Process.
// It kills the process and PTYCmd upon cleanup
func Start(t *testing.T, cmd *pty.Cmd, opts ...pty.StartOption) (*PTYCmd, pty.Process) {
t.Helper()
ptty, ps, err := pty.Start(cmd, opts...)
require.NoError(t, err)
t.Cleanup(func() {
_ = ps.Kill()
_ = ps.Wait()
})
ex := expecter.New(t, ptty.OutputReader(), cmd.Args[0])
r := &PTYCmd{
Expecter: *ex,
PTYCmd: ptty,
t: t,
}
t.Cleanup(func() {
_ = r.Close()
})
return r, ps
}
//nolint:govet // We don't care about conforming to ReadRune() (rune, int, error).
type PTY struct {
expecter.Expecter
pty.PTY
t *testing.T
closeOnce sync.Once
closeErr error
}
func (p *PTY) Close() error {
p.t.Helper()
p.closeOnce.Do(func() {
pErr := p.PTY.Close()
if pErr != nil {
p.Logf("PTY: Close failed: %v", pErr)
}
p.Expecter.Close("PTY close")
if pErr != nil {
p.closeErr = pErr
}
})
return p.closeErr
}
func (p *PTY) Attach(inv *serpent.Invocation) *PTY {
p.t.Helper()
inv.Stdout = p.Output()
inv.Stderr = p.Output()
inv.Stdin = p.Input()
return p
}
func (p *PTY) Write(r rune) {
p.t.Helper()
p.Logf("stdin: %q", r)
_, err := p.Input().Write([]byte{byte(r)})
require.NoError(p.t, err, "write failed")
}
func (p *PTY) WriteLine(str string) {
p.t.Helper()
newline := []byte{'\r'}
if runtime.GOOS == "windows" {
newline = append(newline, '\n')
}
p.Logf("stdin: %q", str+string(newline))
_, err := p.Input().Write(append([]byte(str), newline...))
require.NoError(p.t, err, "write line failed")
}
// Named sets the PTY name in the logs. Defaults to "cmd". Make sure you set this before anything starts writing to the
// pty, or it may not be named consistently. E.g.
//
// p := New(t).Named("myCmd")
func (p *PTY) Named(name string) *PTY {
p.Rename(name)
return p
}
type PTYCmd struct {
expecter.Expecter
pty.PTYCmd
t *testing.T
}
func (p *PTYCmd) Close() error {
p.t.Helper()
pErr := p.PTYCmd.Close()
if pErr != nil {
p.Logf("PTYCmd: Close failed: %v", pErr)
}
p.Expecter.Close("PTYCmd close")
return pErr
}