Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 80a0055

Browse files
committed
fix: do terminal emulation in reconnecting pty tests
It looks like it is possible for screen to use control sequences instead of literal newlines which fails the tests. This reuses the existing readUntil function used in other pty tests.
1 parent c2c9da7 commit 80a0055

File tree

4 files changed

+85
-99
lines changed

4 files changed

+85
-99
lines changed

agent/agent_test.go

Lines changed: 11 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package agent_test
22

33
import (
4-
"bufio"
54
"bytes"
65
"context"
76
"encoding/json"
@@ -1588,10 +1587,6 @@ func TestAgent_Startup(t *testing.T) {
15881587
})
15891588
}
15901589

1591-
const ansi = "[\u001B\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)*)?\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PRZcf-ntqry=><~]))"
1592-
1593-
var re = regexp.MustCompile(ansi)
1594-
15951590
//nolint:paralleltest // This test sets an environment variable.
15961591
func TestAgent_ReconnectingPTY(t *testing.T) {
15971592
if runtime.GOOS == "windows" {
@@ -1639,13 +1634,10 @@ func TestAgent_ReconnectingPTY(t *testing.T) {
16391634
require.NoError(t, err)
16401635
defer netConn1.Close()
16411636

1642-
scanner1 := bufio.NewScanner(netConn1)
1643-
16441637
// A second simultaneous connection.
16451638
netConn2, err := conn.ReconnectingPTY(ctx, id, 100, 100, "bash")
16461639
require.NoError(t, err)
16471640
defer netConn2.Close()
1648-
scanner2 := bufio.NewScanner(netConn2)
16491641

16501642
// Brief pause to reduce the likelihood that we send keystrokes while
16511643
// the shell is simultaneously sending a prompt.
@@ -1658,17 +1650,6 @@ func TestAgent_ReconnectingPTY(t *testing.T) {
16581650
_, err = netConn1.Write(data)
16591651
require.NoError(t, err)
16601652

1661-
hasLine := func(scanner *bufio.Scanner, matcher func(string) bool) bool {
1662-
for scanner.Scan() {
1663-
line := scanner.Text()
1664-
t.Logf("bash tty stdout = %s", re.ReplaceAllString(line, ""))
1665-
if matcher(line) {
1666-
return true
1667-
}
1668-
}
1669-
return false
1670-
}
1671-
16721653
matchEchoCommand := func(line string) bool {
16731654
return strings.Contains(line, "echo test")
16741655
}
@@ -1683,25 +1664,23 @@ func TestAgent_ReconnectingPTY(t *testing.T) {
16831664
}
16841665

16851666
// Once for typing the command...
1686-
require.True(t, hasLine(scanner1, matchEchoCommand), "find echo command")
1667+
require.NoError(t, testutil.ReadUntil(ctx, t, netConn1, matchEchoCommand), "find echo command")
16871668
// And another time for the actual output.
1688-
require.True(t, hasLine(scanner1, matchEchoOutput), "find echo output")
1669+
require.NoError(t, testutil.ReadUntil(ctx, t, netConn1, matchEchoOutput), "find echo output")
16891670

16901671
// Same for the other connection.
1691-
require.True(t, hasLine(scanner2, matchEchoCommand), "find echo command")
1692-
require.True(t, hasLine(scanner2, matchEchoOutput), "find echo output")
1672+
require.NoError(t, testutil.ReadUntil(ctx, t, netConn2, matchEchoCommand), "find echo command")
1673+
require.NoError(t, testutil.ReadUntil(ctx, t, netConn2, matchEchoOutput), "find echo output")
16931674

16941675
_ = netConn1.Close()
16951676
_ = netConn2.Close()
16961677
netConn3, err := conn.ReconnectingPTY(ctx, id, 100, 100, "bash")
16971678
require.NoError(t, err)
16981679
defer netConn3.Close()
16991680

1700-
scanner3 := bufio.NewScanner(netConn3)
1701-
17021681
// Same output again!
1703-
require.True(t, hasLine(scanner3, matchEchoCommand), "find echo command")
1704-
require.True(t, hasLine(scanner3, matchEchoOutput), "find echo output")
1682+
require.NoError(t, testutil.ReadUntil(ctx, t, netConn3, matchEchoCommand), "find echo command")
1683+
require.NoError(t, testutil.ReadUntil(ctx, t, netConn3, matchEchoOutput), "find echo output")
17051684

17061685
// Exit should cause the connection to close.
17071686
data, err = json.Marshal(codersdk.ReconnectingPTYRequest{
@@ -1712,26 +1691,19 @@ func TestAgent_ReconnectingPTY(t *testing.T) {
17121691
require.NoError(t, err)
17131692

17141693
// Once for the input and again for the output.
1715-
require.True(t, hasLine(scanner3, matchExitCommand), "find exit command")
1716-
require.True(t, hasLine(scanner3, matchExitOutput), "find exit output")
1694+
require.NoError(t, testutil.ReadUntil(ctx, t, netConn3, matchExitCommand), "find exit command")
1695+
require.NoError(t, testutil.ReadUntil(ctx, t, netConn3, matchExitOutput), "find exit output")
17171696

17181697
// Wait for the connection to close.
1719-
for scanner3.Scan() {
1720-
line := scanner3.Text()
1721-
t.Logf("bash tty stdout = %s", re.ReplaceAllString(line, ""))
1722-
}
1698+
require.ErrorIs(t, testutil.ReadUntil(ctx, t, netConn3, nil), io.EOF)
17231699

17241700
// Try a non-shell command. It should output then immediately exit.
17251701
netConn4, err := conn.ReconnectingPTY(ctx, uuid.New(), 100, 100, "echo test")
17261702
require.NoError(t, err)
17271703
defer netConn4.Close()
17281704

1729-
scanner4 := bufio.NewScanner(netConn4)
1730-
require.True(t, hasLine(scanner4, matchEchoOutput), "find echo output")
1731-
for scanner4.Scan() {
1732-
line := scanner4.Text()
1733-
t.Logf("bash tty stdout = %s", re.ReplaceAllString(line, ""))
1734-
}
1705+
require.NoError(t, testutil.ReadUntil(ctx, t, netConn4, matchEchoOutput), "find echo output")
1706+
require.ErrorIs(t, testutil.ReadUntil(ctx, t, netConn3, nil), io.EOF)
17351707
})
17361708
}
17371709
}

coderd/workspaceapps/apptest/apptest.go

Lines changed: 5 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import (
1212
"net/http/httputil"
1313
"net/url"
1414
"path"
15-
"regexp"
1615
"runtime"
1716
"strconv"
1817
"strings"
@@ -31,10 +30,6 @@ import (
3130
"github.com/coder/coder/testutil"
3231
)
3332

34-
const ansi = "[\u001B\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[a-zA-Z\\d]*)*)?\u0007)|(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PRZcf-ntqry=><~]))"
35-
36-
var re = regexp.MustCompile(ansi)
37-
3833
// Run runs the entire workspace app test suite against deployments minted
3934
// by the provided factory.
4035
//
@@ -1345,16 +1340,6 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) {
13451340
}
13461341

13471342
func testReconnectingPTY(ctx context.Context, t *testing.T, client *codersdk.Client, opts codersdk.WorkspaceAgentReconnectingPTYOpts) {
1348-
hasLine := func(scanner *bufio.Scanner, matcher func(string) bool) bool {
1349-
for scanner.Scan() {
1350-
line := scanner.Text()
1351-
t.Logf("bash tty stdout = %s", re.ReplaceAllString(line, ""))
1352-
if matcher(line) {
1353-
return true
1354-
}
1355-
}
1356-
return false
1357-
}
13581343
matchEchoCommand := func(line string) bool {
13591344
return strings.Contains(line, "echo test")
13601345
}
@@ -1381,7 +1366,6 @@ func testReconnectingPTY(ctx context.Context, t *testing.T, client *codersdk.Cli
13811366
require.NoError(t, err)
13821367
_, err = conn.Write(data)
13831368
require.NoError(t, err)
1384-
scanner := bufio.NewScanner(conn)
13851369

13861370
// Brief pause to reduce the likelihood that we send keystrokes while
13871371
// the shell is simultaneously sending a prompt.
@@ -1394,8 +1378,8 @@ func testReconnectingPTY(ctx context.Context, t *testing.T, client *codersdk.Cli
13941378
_, err = conn.Write(data)
13951379
require.NoError(t, err)
13961380

1397-
require.True(t, hasLine(scanner, matchEchoCommand), "find echo command")
1398-
require.True(t, hasLine(scanner, matchEchoOutput), "find echo output")
1381+
require.NoError(t, testutil.ReadUntil(ctx, t, conn, matchEchoCommand), "find echo command")
1382+
require.NoError(t, testutil.ReadUntil(ctx, t, conn, matchEchoOutput), "find echo output")
13991383

14001384
// Exit should cause the connection to close.
14011385
data, err = json.Marshal(codersdk.ReconnectingPTYRequest{
@@ -1406,12 +1390,9 @@ func testReconnectingPTY(ctx context.Context, t *testing.T, client *codersdk.Cli
14061390
require.NoError(t, err)
14071391

14081392
// Once for the input and again for the output.
1409-
require.True(t, hasLine(scanner, matchExitCommand), "find exit command")
1410-
require.True(t, hasLine(scanner, matchExitOutput), "find exit output")
1393+
require.NoError(t, testutil.ReadUntil(ctx, t, conn, matchExitCommand), "find exit command")
1394+
require.NoError(t, testutil.ReadUntil(ctx, t, conn, matchExitOutput), "find exit output")
14111395

14121396
// Ensure the connection closes.
1413-
for scanner.Scan() {
1414-
line := scanner.Text()
1415-
t.Logf("bash tty stdout = %s", re.ReplaceAllString(line, ""))
1416-
}
1397+
require.ErrorIs(t, testutil.ReadUntil(ctx, t, conn, nil), io.EOF)
14171398
}

pty/start_test.go

Lines changed: 1 addition & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,9 @@ import (
55
"context"
66
"fmt"
77
"io"
8-
"strings"
98
"testing"
109
"time"
1110

12-
"github.com/hinshun/vt10x"
1311
"github.com/stretchr/testify/assert"
1412
"github.com/stretchr/testify/require"
1513

@@ -73,7 +71,7 @@ func Test_Start_truncation(t *testing.T) {
7371
n := 1
7472
for n <= countEnd {
7573
want := fmt.Sprintf("%d", n)
76-
err := readUntil(ctx, t, want, pc.OutputReader())
74+
err := testutil.ReadUntilString(ctx, t, want, pc.OutputReader())
7775
assert.NoError(t, err, "want: %s", want)
7876
if err != nil {
7977
return
@@ -141,36 +139,3 @@ func Test_Start_cancel_context(t *testing.T) {
141139
t.Error("cmd.Wait() timed out")
142140
}
143141
}
144-
145-
// readUntil reads one byte at a time until we either see the string we want, or the context expires
146-
func readUntil(ctx context.Context, t *testing.T, want string, r io.Reader) error {
147-
// output can contain virtual terminal sequences, so we need to parse these
148-
// to correctly interpret getting what we want.
149-
term := vt10x.New(vt10x.WithSize(80, 80))
150-
readErrs := make(chan error, 1)
151-
for {
152-
b := make([]byte, 1)
153-
go func() {
154-
_, err := r.Read(b)
155-
readErrs <- err
156-
}()
157-
select {
158-
case err := <-readErrs:
159-
if err != nil {
160-
t.Logf("err: %v\ngot: %v", err, term)
161-
return err
162-
}
163-
term.Write(b)
164-
case <-ctx.Done():
165-
return ctx.Err()
166-
}
167-
got := term.String()
168-
lines := strings.Split(got, "\n")
169-
for _, line := range lines {
170-
if strings.TrimSpace(line) == want {
171-
t.Logf("want: %v\n got:%v", want, line)
172-
return nil
173-
}
174-
}
175-
}
176-
}

testutil/pty.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package testutil
2+
3+
import (
4+
"context"
5+
"io"
6+
"strings"
7+
"testing"
8+
9+
"github.com/hinshun/vt10x"
10+
)
11+
12+
// ReadUntilString emulates a terminal and reads one byte at a time until we
13+
// either see the string we want, or the context expires.
14+
func ReadUntilString(ctx context.Context, t *testing.T, want string, r io.Reader) error {
15+
return ReadUntil(ctx, t, r, func(line string) bool {
16+
if strings.TrimSpace(line) == want {
17+
t.Logf("want: %v\n got:%v", want, line)
18+
return true
19+
}
20+
return false
21+
})
22+
}
23+
24+
// ReadUntil emulates a terminal and reads one byte at a time until matcher
25+
// returns true or the context expires. If the matcher is nil, read until EOF.
26+
func ReadUntil(ctx context.Context, t *testing.T, r io.Reader, matcher func(line string) bool) error {
27+
// output can contain virtual terminal sequences, so we need to parse these
28+
// to correctly interpret getting what we want.
29+
term := vt10x.New(vt10x.WithSize(80, 80))
30+
readErrs := make(chan error, 1)
31+
defer func() {
32+
// Dump the terminal contents since they can be helpful for debugging, but
33+
// skip empty lines since much of the terminal will usually be blank.
34+
got := term.String()
35+
lines := strings.Split(got, "\n")
36+
for _, line := range lines {
37+
if strings.TrimSpace(line) != "" {
38+
t.Logf("got: %v", line)
39+
}
40+
}
41+
}()
42+
for {
43+
b := make([]byte, 1)
44+
go func() {
45+
_, err := r.Read(b)
46+
readErrs <- err
47+
}()
48+
select {
49+
case err := <-readErrs:
50+
if err != nil {
51+
return err
52+
}
53+
_, err = term.Write(b)
54+
if err != nil {
55+
return err
56+
}
57+
case <-ctx.Done():
58+
return ctx.Err()
59+
}
60+
got := term.String()
61+
lines := strings.Split(got, "\n")
62+
for _, line := range lines {
63+
if matcher != nil && matcher(line) {
64+
return nil
65+
}
66+
}
67+
}
68+
}

0 commit comments

Comments
 (0)