Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
gitpod-io
GitHub Repository: gitpod-io/gitpod
Path: blob/main/components/supervisor/pkg/terminal/terminal_test.go
2500 views
1
// Copyright (c) 2020 Gitpod GmbH. All rights reserved.
2
// Licensed under the GNU Affero General Public License (AGPL).
3
// See License.AGPL.txt in the project root for license information.
4
5
package terminal
6
7
import (
8
"bytes"
9
"context"
10
"io"
11
"os"
12
"os/exec"
13
"strings"
14
"testing"
15
"time"
16
17
"github.com/google/go-cmp/cmp"
18
"golang.org/x/sync/errgroup"
19
"google.golang.org/grpc"
20
21
"github.com/gitpod-io/gitpod/supervisor/api"
22
)
23
24
func TestTitle(t *testing.T) {
25
t.Skip("skipping flakey tests")
26
27
tests := []struct {
28
Desc string
29
Title string
30
Command string
31
Default string
32
Expectation string
33
}{
34
{
35
Desc: "with args",
36
Command: "watch ls",
37
Default: "bash",
38
Expectation: "watch",
39
},
40
{
41
Desc: "with predefined title",
42
Title: "run app",
43
Command: "sh",
44
Default: "run app: bash",
45
Expectation: "run app: sh",
46
},
47
}
48
for _, test := range tests {
49
t.Run(test.Desc, func(t *testing.T) {
50
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
51
defer cancel()
52
53
mux := NewMux()
54
defer mux.Close(ctx)
55
56
tmpWorkdir, err := os.MkdirTemp("", "workdirectory")
57
if err != nil {
58
t.Fatal(err)
59
}
60
defer os.RemoveAll(tmpWorkdir)
61
62
terminalService := NewMuxTerminalService(mux)
63
terminalService.DefaultWorkdir = tmpWorkdir
64
65
term, err := terminalService.OpenWithOptions(ctx, &api.OpenTerminalRequest{}, TermOptions{
66
Title: test.Title,
67
})
68
if err != nil {
69
t.Fatal(err)
70
}
71
72
if diff := cmp.Diff(test.Default, term.Terminal.Title); diff != "" {
73
t.Errorf("unexpected output (-want +got):\n%s", diff)
74
}
75
76
listener := &TestTitleTerminalServiceListener{
77
ctx: ctx,
78
resps: make(chan *api.ListenTerminalResponse),
79
}
80
titles := listener.Titles(2)
81
go func() {
82
//nolint:errcheck
83
terminalService.Listen(&api.ListenTerminalRequest{Alias: term.Terminal.Alias}, listener)
84
}()
85
86
// initial event could contain not contain updates
87
time.Sleep(100 * time.Millisecond)
88
89
title := <-titles
90
if diff := cmp.Diff(test.Default, title); diff != "" {
91
t.Errorf("unexpected output (-want +got):\n%s", diff)
92
}
93
94
_, err = terminalService.Write(ctx, &api.WriteTerminalRequest{Alias: term.Terminal.Alias, Stdin: []byte(test.Command + "\r\n")})
95
if err != nil {
96
t.Fatal(err)
97
}
98
99
_, err = terminalService.Shutdown(ctx, &api.ShutdownTerminalRequest{Alias: term.Terminal.Alias})
100
if err != nil {
101
t.Fatal(err)
102
}
103
104
title = <-titles
105
if diff := cmp.Diff(test.Expectation, title); diff != "" {
106
t.Errorf("unexpected output (-want +got):\n%s", diff)
107
}
108
})
109
}
110
}
111
112
type TestTitleTerminalServiceListener struct {
113
ctx context.Context
114
resps chan *api.ListenTerminalResponse
115
grpc.ServerStream
116
}
117
118
func (listener *TestTitleTerminalServiceListener) Send(resp *api.ListenTerminalResponse) error {
119
listener.resps <- resp
120
return nil
121
}
122
123
func (listener *TestTitleTerminalServiceListener) Context() context.Context {
124
return listener.ctx
125
}
126
127
func (listener *TestTitleTerminalServiceListener) Titles(size int) chan string {
128
title := make(chan string, size)
129
go func() {
130
//nolint:gosimple
131
for {
132
select {
133
case resp := <-listener.resps:
134
{
135
titleChanged, ok := resp.Output.(*api.ListenTerminalResponse_Title)
136
if ok {
137
title <- titleChanged.Title
138
break
139
}
140
}
141
}
142
}
143
}()
144
return title
145
}
146
147
func TestAnnotations(t *testing.T) {
148
tests := []struct {
149
Desc string
150
Req *api.OpenTerminalRequest
151
Opts *TermOptions
152
Expectation map[string]string
153
}{
154
{
155
Desc: "no annotations",
156
Req: &api.OpenTerminalRequest{
157
Annotations: map[string]string{},
158
},
159
Expectation: map[string]string{},
160
},
161
{
162
Desc: "request annotation",
163
Req: &api.OpenTerminalRequest{
164
Annotations: map[string]string{
165
"hello": "world",
166
},
167
},
168
Expectation: map[string]string{
169
"hello": "world",
170
},
171
},
172
{
173
Desc: "option annotation",
174
Req: &api.OpenTerminalRequest{
175
Annotations: map[string]string{
176
"hello": "world",
177
},
178
},
179
Opts: &TermOptions{
180
Annotations: map[string]string{
181
"hello": "foo",
182
"bar": "baz",
183
},
184
},
185
Expectation: map[string]string{
186
"hello": "world",
187
"bar": "baz",
188
},
189
},
190
}
191
192
for _, test := range tests {
193
t.Run(test.Desc, func(t *testing.T) {
194
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
195
defer cancel()
196
197
mux := NewMux()
198
defer mux.Close(ctx)
199
200
terminalService := NewMuxTerminalService(mux)
201
var err error
202
if test.Opts == nil {
203
_, err = terminalService.Open(ctx, test.Req)
204
} else {
205
_, err = terminalService.OpenWithOptions(ctx, test.Req, *test.Opts)
206
}
207
if err != nil {
208
t.Fatal(err)
209
return
210
}
211
212
lr, err := terminalService.List(ctx, &api.ListTerminalsRequest{})
213
if err != nil {
214
t.Fatal(err)
215
return
216
}
217
if len(lr.Terminals) != 1 {
218
t.Fatalf("expected exactly one terminal, got %d", len(lr.Terminals))
219
return
220
}
221
222
if diff := cmp.Diff(test.Expectation, lr.Terminals[0].Annotations); diff != "" {
223
t.Errorf("unexpected output (-want +got):\n%s", diff)
224
}
225
})
226
}
227
}
228
229
func TestTerminals(t *testing.T) {
230
tests := []struct {
231
Desc string
232
Stdin []string
233
Expectation func(terminal *Term) string
234
}{
235
{
236
Desc: "recorded output should be equals read output",
237
Stdin: []string{
238
"echo \"yarn\"",
239
"echo \"gp sync-done init\"",
240
"echo \"yarn --cwd theia-training watch\"",
241
"history",
242
"exit",
243
},
244
Expectation: func(terminal *Term) string {
245
return string(terminal.Stdout.recorder.Bytes())
246
},
247
},
248
}
249
for _, test := range tests {
250
t.Run(test.Desc, func(t *testing.T) {
251
terminalService := NewMuxTerminalService(NewMux())
252
resp, err := terminalService.Open(context.Background(), &api.OpenTerminalRequest{})
253
if err != nil {
254
t.Fatal(err)
255
}
256
terminal, ok := terminalService.Mux.Get(resp.Terminal.Alias)
257
if !ok {
258
t.Fatal("no terminal")
259
}
260
stdoutOutput := bytes.NewBuffer(nil)
261
262
go func() {
263
// give the io.Copy some time to start
264
time.Sleep(500 * time.Millisecond)
265
266
for _, stdin := range test.Stdin {
267
terminal.PTY.Write([]byte(stdin + "\r\n"))
268
}
269
}()
270
io.Copy(stdoutOutput, terminal.Stdout.Listen())
271
272
expectation := strings.Split(test.Expectation(terminal), "\r\n")
273
actual := strings.Split(stdoutOutput.String(), "\r\n")
274
if diff := cmp.Diff(expectation, actual); diff != "" {
275
t.Errorf("unexpected output (-want +got):\n%s", diff)
276
}
277
})
278
}
279
}
280
281
func TestConcurrent(t *testing.T) {
282
var (
283
terminals = NewMux()
284
terminalCount = 2
285
listenerCount = 2
286
)
287
288
eg, ctx := errgroup.WithContext(context.Background())
289
defer terminals.Close(ctx)
290
for i := 0; i < terminalCount; i++ {
291
alias, err := terminals.Start(exec.Command("/bin/bash", "-i"), TermOptions{
292
ReadTimeout: 0,
293
})
294
if err != nil {
295
t.Fatal(err)
296
}
297
term, ok := terminals.Get(alias)
298
if !ok {
299
t.Fatal("terminal is not found")
300
}
301
302
for j := 0; j < listenerCount; j++ {
303
stdout := term.Stdout.Listen()
304
eg.Go(func() error {
305
buf := new(strings.Builder)
306
_, err = io.Copy(buf, stdout)
307
if err != nil {
308
return err
309
}
310
return nil
311
})
312
}
313
314
_, err = term.PTY.Write([]byte("echo \"Hello World\"; exit\n"))
315
if err != nil {
316
t.Fatal(err)
317
}
318
}
319
err := eg.Wait()
320
if err != nil {
321
t.Fatal(err)
322
}
323
}
324
325
func TestWorkDirProvider(t *testing.T) {
326
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
327
defer cancel()
328
329
mux := NewMux()
330
defer mux.Close(ctx)
331
332
terminalService := NewMuxTerminalService(mux)
333
334
type AssertWorkDirTest struct {
335
expectedWorkDir string
336
providedWorkDir string
337
}
338
assertWorkDir := func(arg *AssertWorkDirTest) {
339
term, err := terminalService.Open(ctx, &api.OpenTerminalRequest{
340
Workdir: arg.providedWorkDir,
341
})
342
if err != nil {
343
t.Fatal(err)
344
}
345
if diff := cmp.Diff(arg.expectedWorkDir, term.Terminal.CurrentWorkdir); diff != "" {
346
t.Errorf("unexpected output (-want +got):\n%s", diff)
347
}
348
_, err = terminalService.Shutdown(ctx, &api.ShutdownTerminalRequest{
349
Alias: term.Terminal.Alias,
350
})
351
if err != nil {
352
t.Fatal(err)
353
}
354
}
355
356
staticWorkDir, err := os.MkdirTemp("", "staticworkdir")
357
if err != nil {
358
t.Fatal(err)
359
}
360
defer os.RemoveAll(staticWorkDir)
361
362
terminalService.DefaultWorkdir = staticWorkDir
363
assertWorkDir(&AssertWorkDirTest{
364
expectedWorkDir: staticWorkDir,
365
})
366
367
dynamicWorkDir := ""
368
terminalService.DefaultWorkdirProvider = func() string {
369
return dynamicWorkDir
370
}
371
assertWorkDir(&AssertWorkDirTest{
372
expectedWorkDir: staticWorkDir,
373
})
374
375
dynamicWorkDir, err = os.MkdirTemp("", "dynamicworkdir")
376
if err != nil {
377
t.Fatal(err)
378
}
379
defer os.RemoveAll(dynamicWorkDir)
380
assertWorkDir(&AssertWorkDirTest{
381
expectedWorkDir: dynamicWorkDir,
382
})
383
384
providedWorkDir, err := os.MkdirTemp("", "providedworkdir")
385
if err != nil {
386
t.Fatal(err)
387
}
388
defer os.RemoveAll(providedWorkDir)
389
assertWorkDir(&AssertWorkDirTest{
390
providedWorkDir: providedWorkDir,
391
expectedWorkDir: providedWorkDir,
392
})
393
}
394
395