@@ -7,116 +7,247 @@ import (
7
7
"io"
8
8
"os"
9
9
"os/exec"
10
- "regexp"
11
10
"runtime"
12
11
"strings"
12
+ "sync"
13
13
"testing"
14
14
"time"
15
15
"unicode/utf8"
16
16
17
17
"github.com/stretchr/testify/require"
18
+ "golang.org/x/xerrors"
18
19
19
20
"github.com/coder/coder/pty"
20
21
)
21
22
22
- var (
23
- // Used to ensure terminal output doesn't have anything crazy!
24
- // See: https://stackoverflow.com/a/29497680
25
- stripAnsi = regexp .MustCompile ("[\u001B \u009B ][[\\ ]()#;?]*(?:(?:(?:[a-zA-Z\\ d]*(?:;[a-zA-Z\\ d]*)*)?\u0007 )|(?:(?:\\ d{1,4}(?:;\\ d{0,4})*)?[\\ dA-PRZcf-ntqry=><~]))" )
26
- )
27
-
28
23
func New (t * testing.T ) * PTY {
29
24
ptty , err := pty .New ()
30
25
require .NoError (t , err )
31
26
32
- return create (t , ptty )
27
+ return create (t , ptty , "cmd" )
33
28
}
34
29
35
30
func Start (t * testing.T , cmd * exec.Cmd ) (* PTY , * os.Process ) {
36
31
ptty , ps , err := pty .Start (cmd )
37
32
require .NoError (t , err )
38
- return create (t , ptty ), ps
33
+ return create (t , ptty , cmd . Args [ 0 ] ), ps
39
34
}
40
35
41
- func create (t * testing.T , ptty pty.PTY ) * PTY {
42
- reader , writer := io .Pipe ()
43
- scanner := bufio .NewScanner (reader )
36
+ func create (t * testing.T , ptty pty.PTY , name string ) * PTY {
37
+ // Use pipe for logging.
38
+ logDone := make (chan struct {})
39
+ logr , logw := io .Pipe ()
44
40
t .Cleanup (func () {
45
- _ = reader .Close ()
46
- _ = writer .Close ()
41
+ _ = logw .Close ()
42
+ _ = logr .Close ()
43
+ <- logDone // Guard against logging after test.
47
44
})
48
45
go func () {
49
- for scanner . Scan () {
50
- if scanner . Err () != nil {
51
- return
52
- }
53
- t .Log ( stripAnsi . ReplaceAllString ( scanner . Text (), "" ))
46
+ defer close ( logDone )
47
+ s := bufio . NewScanner ( logr )
48
+ for s . Scan () {
49
+ // Quote output to avoid terminal escape codes, e.g. bell.
50
+ t .Logf ( "%s: stdout: %q" , name , s . Text ())
54
51
}
55
52
}()
56
53
54
+ // Write to log and output buffer.
55
+ copyDone := make (chan struct {})
56
+ out := newStdbuf ()
57
+ w := io .MultiWriter (logw , out )
58
+ go func () {
59
+ defer close (copyDone )
60
+ _ , err := io .Copy (w , ptty .Output ())
61
+ _ = out .closeErr (err )
62
+ }()
57
63
t .Cleanup (func () {
64
+ _ = out .Close
58
65
_ = ptty .Close ()
66
+ <- copyDone
59
67
})
68
+
60
69
return & PTY {
61
70
t : t ,
62
71
PTY : ptty ,
72
+ out : out ,
63
73
64
- outputWriter : writer ,
65
- runeReader : bufio .NewReaderSize (ptty .Output (), utf8 .UTFMax ),
74
+ runeReader : bufio .NewReaderSize (out , utf8 .UTFMax ),
66
75
}
67
76
}
68
77
69
78
type PTY struct {
70
79
t * testing.T
71
80
pty.PTY
81
+ out * stdbuf
72
82
73
- outputWriter io.Writer
74
- runeReader * bufio.Reader
83
+ runeReader * bufio.Reader
75
84
}
76
85
77
86
func (p * PTY ) ExpectMatch (str string ) string {
78
- var buffer bytes.Buffer
79
- multiWriter := io .MultiWriter (& buffer , p .outputWriter )
80
- runeWriter := bufio .NewWriterSize (multiWriter , utf8 .UTFMax )
87
+ p .t .Helper ()
88
+
81
89
complete , cancelFunc := context .WithCancel (context .Background ())
82
90
defer cancelFunc ()
91
+
92
+ timeout := make (chan error , 1 )
83
93
go func () {
94
+ defer close (timeout )
84
95
timer := time .NewTimer (10 * time .Second )
85
96
defer timer .Stop ()
86
97
select {
87
98
case <- complete .Done ():
88
99
return
89
100
case <- timer .C :
90
101
}
91
- _ = p .Close ()
92
- p .t .Errorf ("%s match exceeded deadline: wanted %q; got %q" , time .Now (), str , buffer .String ())
102
+ timeout <- xerrors .Errorf ("%s match exceeded deadline" , time .Now ())
93
103
}()
94
- for {
95
- var r rune
96
- r , _ , err := p .runeReader .ReadRune ()
97
- require .NoError (p .t , err )
98
- _ , err = runeWriter .WriteRune (r )
99
- require .NoError (p .t , err )
100
- err = runeWriter .Flush ()
101
- require .NoError (p .t , err )
102
- if strings .Contains (buffer .String (), str ) {
103
- break
104
+
105
+ var buffer bytes.Buffer
106
+ match := make (chan error , 1 )
107
+ go func () {
108
+ defer close (match )
109
+ for {
110
+ r , _ , err := p .runeReader .ReadRune ()
111
+ if err != nil {
112
+ match <- err
113
+ return
114
+ }
115
+ _ , err = buffer .WriteRune (r )
116
+ if err != nil {
117
+ match <- err
118
+ return
119
+ }
120
+ if strings .Contains (buffer .String (), str ) {
121
+ match <- nil
122
+ return
123
+ }
124
+ }
125
+ }()
126
+
127
+ select {
128
+ case err := <- match :
129
+ if err != nil {
130
+ p .t .Fatalf ("read error: %v (wanted %q; got %q)" , err , str , buffer .String ())
104
131
}
132
+ p .t .Logf ("matched %q = %q" , str , buffer .String ())
133
+ case err := <- timeout :
134
+ _ = p .out .closeErr (p .Close ())
135
+ p .t .Fatalf ("%s: wanted %q; got %q" , err , str , buffer .String ())
105
136
}
106
- p .t .Logf ("matched %q = %q" , str , stripAnsi .ReplaceAllString (buffer .String (), "" ))
107
137
return buffer .String ()
108
138
}
109
139
110
140
func (p * PTY ) Write (r rune ) {
141
+ p .t .Helper ()
142
+
111
143
_ , err := p .Input ().Write ([]byte {byte (r )})
112
144
require .NoError (p .t , err )
113
145
}
114
146
115
147
func (p * PTY ) WriteLine (str string ) {
148
+ p .t .Helper ()
149
+
116
150
newline := []byte {'\r' }
117
151
if runtime .GOOS == "windows" {
118
152
newline = append (newline , '\n' )
119
153
}
120
154
_ , err := p .Input ().Write (append ([]byte (str ), newline ... ))
121
155
require .NoError (p .t , err )
122
156
}
157
+
158
+ // stdbuf is like a buffered stdout, it buffers writes until read.
159
+ type stdbuf struct {
160
+ r io.Reader
161
+
162
+ mu sync.Mutex // Protects following.
163
+ b []byte
164
+ more chan struct {}
165
+ err error
166
+ }
167
+
168
+ func newStdbuf () * stdbuf {
169
+ return & stdbuf {more : make (chan struct {}, 1 )}
170
+ }
171
+
172
+ func (b * stdbuf ) Read (p []byte ) (int , error ) {
173
+ if b .r == nil {
174
+ return b .read (p )
175
+ }
176
+
177
+ n , err := b .r .Read (p )
178
+ if xerrors .Is (err , io .EOF ) {
179
+ b .r = nil
180
+ err = nil
181
+ if n == 0 {
182
+ return b .read (p )
183
+ }
184
+ }
185
+ return n , err
186
+ }
187
+
188
+ func (b * stdbuf ) read (p []byte ) (int , error ) {
189
+ b .mu .Lock ()
190
+ defer b .mu .Unlock ()
191
+
192
+ // Deplete channel so that more check
193
+ // is for future input into buffer.
194
+ select {
195
+ case <- b .more :
196
+ default :
197
+ }
198
+
199
+ if len (b .b ) == 0 {
200
+ if b .err != nil {
201
+ return 0 , b .err
202
+ }
203
+
204
+ b .mu .Unlock ()
205
+ <- b .more
206
+ b .mu .Lock ()
207
+ }
208
+
209
+ b .r = bytes .NewReader (b .b )
210
+ b .b = b .b [len (b .b ):]
211
+
212
+ return b .r .Read (p )
213
+ }
214
+
215
+ func (b * stdbuf ) Write (p []byte ) (int , error ) {
216
+ if len (p ) == 0 {
217
+ return 0 , nil
218
+ }
219
+
220
+ b .mu .Lock ()
221
+ defer b .mu .Unlock ()
222
+
223
+ if b .err != nil {
224
+ return 0 , b .err
225
+ }
226
+
227
+ b .b = append (b .b , p ... )
228
+
229
+ select {
230
+ case b .more <- struct {}{}:
231
+ default :
232
+ }
233
+
234
+ return len (p ), nil
235
+ }
236
+
237
+ func (b * stdbuf ) Close () error {
238
+ return b .closeErr (nil )
239
+ }
240
+
241
+ func (b * stdbuf ) closeErr (err error ) error {
242
+ b .mu .Lock ()
243
+ defer b .mu .Unlock ()
244
+ if b .err != nil {
245
+ return err
246
+ }
247
+ if err == nil {
248
+ err = io .EOF
249
+ }
250
+ b .err = err
251
+ close (b .more )
252
+ return b .err
253
+ }
0 commit comments