fix: use terminal emulator that keeps state in ReconnectingPTY tests (#9765)

* Add more pty diagnostics for terminal parsing

Signed-off-by: Spike Curtis <spike@coder.com>

* print escaped strings

Signed-off-by: Spike Curtis <spike@coder.com>

* Only log on failure - heisenbug?

Signed-off-by: Spike Curtis <spike@coder.com>

* use the terminal across matches to keep cursor & contents state

Signed-off-by: Spike Curtis <spike@coder.com>

* Only log bytes if we're not expecting EOF

Signed-off-by: Spike Curtis <spike@coder.com>

---------

Signed-off-by: Spike Curtis <spike@coder.com>
This commit is contained in:
Spike Curtis
2023-09-19 17:57:30 +00:00
committed by GitHub
parent 269b1c59f1
commit 70e481e7a5
4 changed files with 68 additions and 32 deletions
+45 -14
View File
@@ -6,14 +6,33 @@ import (
"strings"
"testing"
"golang.org/x/xerrors"
"github.com/hinshun/vt10x"
)
// TerminalReader emulates a terminal and allows matching output. It's important in cases where we
// can get control sequences to parse them correctly, and keep the state of the terminal across the
// lifespan of the PTY, since some control sequences are relative to the current cursor position.
type TerminalReader struct {
t *testing.T
r io.Reader
term vt10x.Terminal
}
func NewTerminalReader(t *testing.T, r io.Reader) *TerminalReader {
return &TerminalReader{
t: t,
r: r,
term: vt10x.New(vt10x.WithSize(80, 80)),
}
}
// ReadUntilString emulates a terminal and reads one byte at a time until we
// either see the string we want, or the context expires. The PTY must be sized
// to 80x80 or there could be unexpected results.
func ReadUntilString(ctx context.Context, t *testing.T, want string, r io.Reader) error {
return ReadUntil(ctx, t, r, func(line string) bool {
func (tr *TerminalReader) ReadUntilString(ctx context.Context, want string) error {
return tr.ReadUntil(ctx, func(line string) bool {
return strings.TrimSpace(line) == want
})
}
@@ -21,26 +40,37 @@ func ReadUntilString(ctx context.Context, t *testing.T, want string, r io.Reader
// ReadUntil emulates a terminal and reads one byte at a time until the matcher
// returns true or the context expires. If the matcher is nil, read until EOF.
// The PTY must be sized to 80x80 or there could be unexpected results.
func ReadUntil(ctx context.Context, t *testing.T, r io.Reader, matcher func(line string) bool) error {
// output can contain virtual terminal sequences, so we need to parse these
// to correctly interpret getting what we want.
term := vt10x.New(vt10x.WithSize(80, 80))
func (tr *TerminalReader) ReadUntil(ctx context.Context, matcher func(line string) bool) (retErr error) {
readBytes := make([]byte, 0)
readErrs := make(chan error, 1)
defer func() {
// Dump the terminal contents since they can be helpful for debugging, but
// skip empty lines since much of the terminal will usually be blank.
got := term.String()
// trim empty lines since much of the terminal will usually be blank.
got := tr.term.String()
lines := strings.Split(got, "\n")
for _, line := range lines {
if strings.TrimSpace(line) != "" {
t.Logf("got: %v", line)
for i := range lines {
if strings.TrimSpace(lines[i]) != "" {
lines = lines[i:]
break
}
}
for i := len(lines) - 1; i >= 0; i-- {
if strings.TrimSpace(lines[i]) != "" {
lines = lines[:i+1]
break
}
}
gotTrimmed := strings.Join(lines, "\n")
tr.t.Logf("Terminal contents:\n%s", gotTrimmed)
// EOF is expected when matcher == nil
if retErr != nil && !(xerrors.Is(retErr, io.EOF) && matcher == nil) {
tr.t.Logf("Bytes Read: %q", string(readBytes))
}
}()
for {
b := make([]byte, 1)
go func() {
_, err := r.Read(b)
_, err := tr.r.Read(b)
readErrs <- err
}()
select {
@@ -48,7 +78,8 @@ func ReadUntil(ctx context.Context, t *testing.T, r io.Reader, matcher func(line
if err != nil {
return err
}
_, err = term.Write(b)
readBytes = append(readBytes, b...)
_, err = tr.term.Write(b)
if err != nil {
return err
}
@@ -59,7 +90,7 @@ func ReadUntil(ctx context.Context, t *testing.T, r io.Reader, matcher func(line
// A nil matcher means to read until EOF.
continue
}
got := term.String()
got := tr.term.String()
lines := strings.Split(got, "\n")
for _, line := range lines {
if matcher(line) {