@@ -7,116 +7,239 @@ 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 {
87
+ p .t .Helper ()
88
+
89
+ timeout , cancel := context .WithTimeout (context .Background (), 10 * time .Second )
90
+ defer cancel ()
91
+
78
92
var buffer bytes.Buffer
79
- multiWriter := io .MultiWriter (& buffer , p .outputWriter )
80
- runeWriter := bufio .NewWriterSize (multiWriter , utf8 .UTFMax )
81
- complete , cancelFunc := context .WithCancel (context .Background ())
82
- defer cancelFunc ()
93
+ match := make (chan error , 1 )
83
94
go func () {
84
- timer := time .NewTimer (10 * time .Second )
85
- defer timer .Stop ()
86
- select {
87
- case <- complete .Done ():
88
- return
89
- case <- timer .C :
90
- }
91
- _ = p .Close ()
92
- p .t .Errorf ("%s match exceeded deadline: wanted %q; got %q" , time .Now (), str , buffer .String ())
95
+ defer close (match )
96
+ match <- func () error {
97
+ for {
98
+ r , _ , err := p .runeReader .ReadRune ()
99
+ if err != nil {
100
+ return err
101
+ }
102
+ _ , err = buffer .WriteRune (r )
103
+ if err != nil {
104
+ return err
105
+ }
106
+ if strings .Contains (buffer .String (), str ) {
107
+ return nil
108
+ }
109
+ }
110
+ }()
93
111
}()
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
112
+
113
+ select {
114
+ case err := <- match :
115
+ if err != nil {
116
+ p .t .Fatalf ("%s: read error: %v (wanted %q; got %q)" , time .Now (), err , str , buffer .String ())
117
+ return ""
104
118
}
119
+ p .t .Logf ("%s: matched %q = %q" , time .Now (), str , buffer .String ())
120
+ return buffer .String ()
121
+ case <- timeout .Done ():
122
+ // Ensure goroutine is cleaned up before test exit.
123
+ _ = p .out .closeErr (p .Close ())
124
+ <- match
125
+
126
+ p .t .Fatalf ("%s: match exceeded deadline: wanted %q; got %q" , time .Now (), str , buffer .String ())
127
+ return ""
105
128
}
106
- p .t .Logf ("matched %q = %q" , str , stripAnsi .ReplaceAllString (buffer .String (), "" ))
107
- return buffer .String ()
108
129
}
109
130
110
131
func (p * PTY ) Write (r rune ) {
132
+ p .t .Helper ()
133
+
111
134
_ , err := p .Input ().Write ([]byte {byte (r )})
112
135
require .NoError (p .t , err )
113
136
}
114
137
115
138
func (p * PTY ) WriteLine (str string ) {
139
+ p .t .Helper ()
140
+
116
141
newline := []byte {'\r' }
117
142
if runtime .GOOS == "windows" {
118
143
newline = append (newline , '\n' )
119
144
}
120
145
_ , err := p .Input ().Write (append ([]byte (str ), newline ... ))
121
146
require .NoError (p .t , err )
122
147
}
148
+
149
+ // stdbuf is like a buffered stdout, it buffers writes until read.
150
+ type stdbuf struct {
151
+ r io.Reader
152
+
153
+ mu sync.Mutex // Protects following.
154
+ b []byte
155
+ more chan struct {}
156
+ err error
157
+ }
158
+
159
+ func newStdbuf () * stdbuf {
160
+ return & stdbuf {more : make (chan struct {}, 1 )}
161
+ }
162
+
163
+ func (b * stdbuf ) Read (p []byte ) (int , error ) {
164
+ if b .r == nil {
165
+ return b .readOrWaitForMore (p )
166
+ }
167
+
168
+ n , err := b .r .Read (p )
169
+ if xerrors .Is (err , io .EOF ) {
170
+ b .r = nil
171
+ err = nil
172
+ if n == 0 {
173
+ return b .readOrWaitForMore (p )
174
+ }
175
+ }
176
+ return n , err
177
+ }
178
+
179
+ func (b * stdbuf ) readOrWaitForMore (p []byte ) (int , error ) {
180
+ b .mu .Lock ()
181
+ defer b .mu .Unlock ()
182
+
183
+ // Deplete channel so that more check
184
+ // is for future input into buffer.
185
+ select {
186
+ case <- b .more :
187
+ default :
188
+ }
189
+
190
+ if len (b .b ) == 0 {
191
+ if b .err != nil {
192
+ return 0 , b .err
193
+ }
194
+
195
+ b .mu .Unlock ()
196
+ <- b .more
197
+ b .mu .Lock ()
198
+ }
199
+
200
+ b .r = bytes .NewReader (b .b )
201
+ b .b = b .b [len (b .b ):]
202
+
203
+ return b .r .Read (p )
204
+ }
205
+
206
+ func (b * stdbuf ) Write (p []byte ) (int , error ) {
207
+ if len (p ) == 0 {
208
+ return 0 , nil
209
+ }
210
+
211
+ b .mu .Lock ()
212
+ defer b .mu .Unlock ()
213
+
214
+ if b .err != nil {
215
+ return 0 , b .err
216
+ }
217
+
218
+ b .b = append (b .b , p ... )
219
+
220
+ select {
221
+ case b .more <- struct {}{}:
222
+ default :
223
+ }
224
+
225
+ return len (p ), nil
226
+ }
227
+
228
+ func (b * stdbuf ) Close () error {
229
+ return b .closeErr (nil )
230
+ }
231
+
232
+ func (b * stdbuf ) closeErr (err error ) error {
233
+ b .mu .Lock ()
234
+ defer b .mu .Unlock ()
235
+ if b .err != nil {
236
+ return err
237
+ }
238
+ if err == nil {
239
+ b .err = io .EOF
240
+ } else {
241
+ b .err = err
242
+ }
243
+ close (b .more )
244
+ return err
245
+ }
0 commit comments