Path: blob/main/components/supervisor/pkg/terminal/terminal_test.go
2500 views
// Copyright (c) 2020 Gitpod GmbH. All rights reserved.1// Licensed under the GNU Affero General Public License (AGPL).2// See License.AGPL.txt in the project root for license information.34package terminal56import (7"bytes"8"context"9"io"10"os"11"os/exec"12"strings"13"testing"14"time"1516"github.com/google/go-cmp/cmp"17"golang.org/x/sync/errgroup"18"google.golang.org/grpc"1920"github.com/gitpod-io/gitpod/supervisor/api"21)2223func TestTitle(t *testing.T) {24t.Skip("skipping flakey tests")2526tests := []struct {27Desc string28Title string29Command string30Default string31Expectation string32}{33{34Desc: "with args",35Command: "watch ls",36Default: "bash",37Expectation: "watch",38},39{40Desc: "with predefined title",41Title: "run app",42Command: "sh",43Default: "run app: bash",44Expectation: "run app: sh",45},46}47for _, test := range tests {48t.Run(test.Desc, func(t *testing.T) {49ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)50defer cancel()5152mux := NewMux()53defer mux.Close(ctx)5455tmpWorkdir, err := os.MkdirTemp("", "workdirectory")56if err != nil {57t.Fatal(err)58}59defer os.RemoveAll(tmpWorkdir)6061terminalService := NewMuxTerminalService(mux)62terminalService.DefaultWorkdir = tmpWorkdir6364term, err := terminalService.OpenWithOptions(ctx, &api.OpenTerminalRequest{}, TermOptions{65Title: test.Title,66})67if err != nil {68t.Fatal(err)69}7071if diff := cmp.Diff(test.Default, term.Terminal.Title); diff != "" {72t.Errorf("unexpected output (-want +got):\n%s", diff)73}7475listener := &TestTitleTerminalServiceListener{76ctx: ctx,77resps: make(chan *api.ListenTerminalResponse),78}79titles := listener.Titles(2)80go func() {81//nolint:errcheck82terminalService.Listen(&api.ListenTerminalRequest{Alias: term.Terminal.Alias}, listener)83}()8485// initial event could contain not contain updates86time.Sleep(100 * time.Millisecond)8788title := <-titles89if diff := cmp.Diff(test.Default, title); diff != "" {90t.Errorf("unexpected output (-want +got):\n%s", diff)91}9293_, err = terminalService.Write(ctx, &api.WriteTerminalRequest{Alias: term.Terminal.Alias, Stdin: []byte(test.Command + "\r\n")})94if err != nil {95t.Fatal(err)96}9798_, err = terminalService.Shutdown(ctx, &api.ShutdownTerminalRequest{Alias: term.Terminal.Alias})99if err != nil {100t.Fatal(err)101}102103title = <-titles104if diff := cmp.Diff(test.Expectation, title); diff != "" {105t.Errorf("unexpected output (-want +got):\n%s", diff)106}107})108}109}110111type TestTitleTerminalServiceListener struct {112ctx context.Context113resps chan *api.ListenTerminalResponse114grpc.ServerStream115}116117func (listener *TestTitleTerminalServiceListener) Send(resp *api.ListenTerminalResponse) error {118listener.resps <- resp119return nil120}121122func (listener *TestTitleTerminalServiceListener) Context() context.Context {123return listener.ctx124}125126func (listener *TestTitleTerminalServiceListener) Titles(size int) chan string {127title := make(chan string, size)128go func() {129//nolint:gosimple130for {131select {132case resp := <-listener.resps:133{134titleChanged, ok := resp.Output.(*api.ListenTerminalResponse_Title)135if ok {136title <- titleChanged.Title137break138}139}140}141}142}()143return title144}145146func TestAnnotations(t *testing.T) {147tests := []struct {148Desc string149Req *api.OpenTerminalRequest150Opts *TermOptions151Expectation map[string]string152}{153{154Desc: "no annotations",155Req: &api.OpenTerminalRequest{156Annotations: map[string]string{},157},158Expectation: map[string]string{},159},160{161Desc: "request annotation",162Req: &api.OpenTerminalRequest{163Annotations: map[string]string{164"hello": "world",165},166},167Expectation: map[string]string{168"hello": "world",169},170},171{172Desc: "option annotation",173Req: &api.OpenTerminalRequest{174Annotations: map[string]string{175"hello": "world",176},177},178Opts: &TermOptions{179Annotations: map[string]string{180"hello": "foo",181"bar": "baz",182},183},184Expectation: map[string]string{185"hello": "world",186"bar": "baz",187},188},189}190191for _, test := range tests {192t.Run(test.Desc, func(t *testing.T) {193ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)194defer cancel()195196mux := NewMux()197defer mux.Close(ctx)198199terminalService := NewMuxTerminalService(mux)200var err error201if test.Opts == nil {202_, err = terminalService.Open(ctx, test.Req)203} else {204_, err = terminalService.OpenWithOptions(ctx, test.Req, *test.Opts)205}206if err != nil {207t.Fatal(err)208return209}210211lr, err := terminalService.List(ctx, &api.ListTerminalsRequest{})212if err != nil {213t.Fatal(err)214return215}216if len(lr.Terminals) != 1 {217t.Fatalf("expected exactly one terminal, got %d", len(lr.Terminals))218return219}220221if diff := cmp.Diff(test.Expectation, lr.Terminals[0].Annotations); diff != "" {222t.Errorf("unexpected output (-want +got):\n%s", diff)223}224})225}226}227228func TestTerminals(t *testing.T) {229tests := []struct {230Desc string231Stdin []string232Expectation func(terminal *Term) string233}{234{235Desc: "recorded output should be equals read output",236Stdin: []string{237"echo \"yarn\"",238"echo \"gp sync-done init\"",239"echo \"yarn --cwd theia-training watch\"",240"history",241"exit",242},243Expectation: func(terminal *Term) string {244return string(terminal.Stdout.recorder.Bytes())245},246},247}248for _, test := range tests {249t.Run(test.Desc, func(t *testing.T) {250terminalService := NewMuxTerminalService(NewMux())251resp, err := terminalService.Open(context.Background(), &api.OpenTerminalRequest{})252if err != nil {253t.Fatal(err)254}255terminal, ok := terminalService.Mux.Get(resp.Terminal.Alias)256if !ok {257t.Fatal("no terminal")258}259stdoutOutput := bytes.NewBuffer(nil)260261go func() {262// give the io.Copy some time to start263time.Sleep(500 * time.Millisecond)264265for _, stdin := range test.Stdin {266terminal.PTY.Write([]byte(stdin + "\r\n"))267}268}()269io.Copy(stdoutOutput, terminal.Stdout.Listen())270271expectation := strings.Split(test.Expectation(terminal), "\r\n")272actual := strings.Split(stdoutOutput.String(), "\r\n")273if diff := cmp.Diff(expectation, actual); diff != "" {274t.Errorf("unexpected output (-want +got):\n%s", diff)275}276})277}278}279280func TestConcurrent(t *testing.T) {281var (282terminals = NewMux()283terminalCount = 2284listenerCount = 2285)286287eg, ctx := errgroup.WithContext(context.Background())288defer terminals.Close(ctx)289for i := 0; i < terminalCount; i++ {290alias, err := terminals.Start(exec.Command("/bin/bash", "-i"), TermOptions{291ReadTimeout: 0,292})293if err != nil {294t.Fatal(err)295}296term, ok := terminals.Get(alias)297if !ok {298t.Fatal("terminal is not found")299}300301for j := 0; j < listenerCount; j++ {302stdout := term.Stdout.Listen()303eg.Go(func() error {304buf := new(strings.Builder)305_, err = io.Copy(buf, stdout)306if err != nil {307return err308}309return nil310})311}312313_, err = term.PTY.Write([]byte("echo \"Hello World\"; exit\n"))314if err != nil {315t.Fatal(err)316}317}318err := eg.Wait()319if err != nil {320t.Fatal(err)321}322}323324func TestWorkDirProvider(t *testing.T) {325ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)326defer cancel()327328mux := NewMux()329defer mux.Close(ctx)330331terminalService := NewMuxTerminalService(mux)332333type AssertWorkDirTest struct {334expectedWorkDir string335providedWorkDir string336}337assertWorkDir := func(arg *AssertWorkDirTest) {338term, err := terminalService.Open(ctx, &api.OpenTerminalRequest{339Workdir: arg.providedWorkDir,340})341if err != nil {342t.Fatal(err)343}344if diff := cmp.Diff(arg.expectedWorkDir, term.Terminal.CurrentWorkdir); diff != "" {345t.Errorf("unexpected output (-want +got):\n%s", diff)346}347_, err = terminalService.Shutdown(ctx, &api.ShutdownTerminalRequest{348Alias: term.Terminal.Alias,349})350if err != nil {351t.Fatal(err)352}353}354355staticWorkDir, err := os.MkdirTemp("", "staticworkdir")356if err != nil {357t.Fatal(err)358}359defer os.RemoveAll(staticWorkDir)360361terminalService.DefaultWorkdir = staticWorkDir362assertWorkDir(&AssertWorkDirTest{363expectedWorkDir: staticWorkDir,364})365366dynamicWorkDir := ""367terminalService.DefaultWorkdirProvider = func() string {368return dynamicWorkDir369}370assertWorkDir(&AssertWorkDirTest{371expectedWorkDir: staticWorkDir,372})373374dynamicWorkDir, err = os.MkdirTemp("", "dynamicworkdir")375if err != nil {376t.Fatal(err)377}378defer os.RemoveAll(dynamicWorkDir)379assertWorkDir(&AssertWorkDirTest{380expectedWorkDir: dynamicWorkDir,381})382383providedWorkDir, err := os.MkdirTemp("", "providedworkdir")384if err != nil {385t.Fatal(err)386}387defer os.RemoveAll(providedWorkDir)388assertWorkDir(&AssertWorkDirTest{389providedWorkDir: providedWorkDir,390expectedWorkDir: providedWorkDir,391})392}393394395