mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
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.
This commit is contained in:
@@ -19,6 +19,7 @@ import (
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/v2/testutil"
|
||||
"github.com/coder/serpent"
|
||||
)
|
||||
|
||||
func New(t *testing.T, r io.Reader, name string) *Expecter {
|
||||
@@ -68,6 +69,22 @@ func New(t *testing.T, r io.Reader, name string) *Expecter {
|
||||
return ex
|
||||
}
|
||||
|
||||
func NewAttachedToInvocation(t *testing.T, invocation *serpent.Invocation) *Expecter {
|
||||
r, w := io.Pipe()
|
||||
invocation.Stdout = w
|
||||
invocation.Stderr = w
|
||||
e := New(t, r, "cmd")
|
||||
|
||||
t.Cleanup(func() {
|
||||
// Serpent doesn't handle closing stdout after running the Invocation; normally the OS does that automatically when
|
||||
// the process exits. Close it here at the end of the test to ensure we don't leak goroutines reading from the
|
||||
// stdout/stderr.
|
||||
_ = w.Close()
|
||||
e.Close("test end")
|
||||
})
|
||||
return e
|
||||
}
|
||||
|
||||
type Expecter struct {
|
||||
t *testing.T
|
||||
out *stdbuf
|
||||
@@ -84,7 +101,7 @@ func (e *Expecter) Rename(name string) {
|
||||
e.name.Store(name)
|
||||
}
|
||||
|
||||
func (e *Expecter) Close(reason string) error {
|
||||
func (e *Expecter) Close(reason string) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
|
||||
defer cancel()
|
||||
|
||||
@@ -94,6 +111,7 @@ func (e *Expecter) Close(reason string) error {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
e.fatalf("close", "copy did not close in time")
|
||||
return
|
||||
case <-e.copyDone:
|
||||
}
|
||||
|
||||
@@ -102,12 +120,11 @@ func (e *Expecter) Close(reason string) error {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
e.fatalf("close", "log pipe did not close in time")
|
||||
return
|
||||
case <-e.logDone:
|
||||
}
|
||||
|
||||
e.Logf("closed expecter")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *Expecter) logClose(name string, c io.Closer) {
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package testutil
|
||||
|
||||
import (
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gvisor.dev/gvisor/pkg/context"
|
||||
|
||||
"cdr.dev/slog/v3"
|
||||
"github.com/coder/serpent"
|
||||
)
|
||||
|
||||
// Writer wraps an underlying io.Writer and provides friendlier methods to write to it, including logging.
|
||||
type Writer struct {
|
||||
t *testing.T
|
||||
w io.Writer
|
||||
l slog.Logger
|
||||
}
|
||||
|
||||
func NewWriterAttachedToInvocation(t *testing.T, logger slog.Logger, invocation *serpent.Invocation) *Writer {
|
||||
r, w := io.Pipe()
|
||||
invocation.Stdin = r
|
||||
// Close the pipe at the end of the test to ensure any goroutine in the Invocation that reads from stdin won't leak.
|
||||
t.Cleanup(func() {
|
||||
_ = w.Close()
|
||||
})
|
||||
return &Writer{
|
||||
t: t,
|
||||
w: w,
|
||||
l: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (w *Writer) Write(r rune) {
|
||||
w.t.Helper()
|
||||
_, err := w.w.Write([]byte{byte(r)})
|
||||
if assert.NoError(w.t, err, "write failed") {
|
||||
w.l.Debug(context.Background(), "wrote rune", slog.F("rune", r))
|
||||
}
|
||||
}
|
||||
|
||||
func (w *Writer) WriteLine(str string) {
|
||||
w.t.Helper()
|
||||
|
||||
// Always write Windows style endings since our CLI prompt readers trim both out. Note this is *different* than what
|
||||
// PTY-based tests do. On Unix-like operating systems we write a single carriage-return (\r) to delimit a line
|
||||
// and the PTY translates it to a line feed (\n) for the CLI command to read. Here there is no translation.
|
||||
newline := []byte{'\r', '\n'}
|
||||
|
||||
_, err := w.w.Write(append([]byte(str), newline...))
|
||||
if assert.NoError(w.t, err, "write line failed") {
|
||||
w.l.Debug(context.Background(), "wrote line", slog.F("line", str+string(newline)))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user