@@ -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
+ }