From f48c85484eafa9273492f8ac31dca0f53a392006 Mon Sep 17 00:00:00 2001 From: Christian Weichel Date: Fri, 10 Nov 2023 11:17:48 +0100 Subject: [PATCH] Add test for ObserveWorkspaceUntilStarted (#19050) --- components/local-app/pkg/helper/workspace.go | 11 +- .../local-app/pkg/helper/workspace_test.go | 161 ++++++++++++++++++ 2 files changed, 166 insertions(+), 6 deletions(-) create mode 100644 components/local-app/pkg/helper/workspace_test.go diff --git a/components/local-app/pkg/helper/workspace.go b/components/local-app/pkg/helper/workspace.go index f8fde8dd41c4b6..51cf7c810dd091 100644 --- a/components/local-app/pkg/helper/workspace.go +++ b/components/local-app/pkg/helper/workspace.go @@ -163,6 +163,9 @@ func ObserveWorkspaceUntilStarted(ctx context.Context, clnt *client.Gitpod, work } ws := wsInfo.Msg.GetResult() + if ws.Status == nil || ws.Status.Instance == nil || ws.Status.Instance.Status == nil { + return nil, fmt.Errorf("cannot get workspace status") + } if ws.Status.Instance.Status.Phase == v1.WorkspaceInstanceStatus_PHASE_RUNNING { // workspace is running - we're done return ws.Status, nil @@ -177,21 +180,18 @@ func ObserveWorkspaceUntilStarted(ctx context.Context, clnt *client.Gitpod, work var ( maxRetries = 5 - retries = 0 delay = 100 * time.Millisecond ) - for { + for retries := 0; retries < maxRetries; retries++ { stream, err := clnt.Workspaces.StreamWorkspaceStatus(ctx, connect.NewRequest(&v1.StreamWorkspaceStatusRequest{WorkspaceId: workspaceID})) if err != nil { if retries >= maxRetries { return nil, prettyprint.MarkExceptional(fmt.Errorf("failed to stream workspace status after %d retries: %w", maxRetries, err)) } - retries++ delay *= 2 slog.Warn("failed to stream workspace status, retrying", "err", err, "retry", retries, "maxRetries", maxRetries) continue } - defer stream.Close() for stream.Receive() { @@ -225,7 +225,6 @@ func ObserveWorkspaceUntilStarted(ctx context.Context, clnt *client.Gitpod, work slog.Warn("failed to stream workspace status, retrying", "err", err, "retry", retries, "maxRetries", maxRetries) continue } - - return nil, prettyprint.MarkExceptional(fmt.Errorf("workspace stream ended unexpectedly")) } + return nil, prettyprint.MarkExceptional(fmt.Errorf("workspace stream ended unexpectedly")) } diff --git a/components/local-app/pkg/helper/workspace_test.go b/components/local-app/pkg/helper/workspace_test.go new file mode 100644 index 00000000000000..cc48714676cbcd --- /dev/null +++ b/components/local-app/pkg/helper/workspace_test.go @@ -0,0 +1,161 @@ +// Copyright (c) 2023 Gitpod GmbH. All rights reserved. +// Licensed under the GNU Affero General Public License (AGPL). +// See License.AGPL.txt in the project root for license information. + +package helper + +import ( + "context" + "fmt" + "net/http" + "net/http/httptest" + "testing" + + "github.com/bufbuild/connect-go" + "github.com/gitpod-io/gitpod/components/public-api/go/client" + v1 "github.com/gitpod-io/gitpod/components/public-api/go/experimental/v1" + gitpod_experimental_v1connect "github.com/gitpod-io/gitpod/components/public-api/go/experimental/v1/v1connect" + "github.com/gitpod-io/local-app/pkg/prettyprint" + "github.com/google/go-cmp/cmp" +) + +func TestObserveWorkspaceUntilStarted(t *testing.T) { + workspaceWithStatus := func(id string, phase v1.WorkspaceInstanceStatus_Phase) *v1.Workspace { + return &v1.Workspace{ + WorkspaceId: id, + Status: &v1.WorkspaceStatus{ + Instance: &v1.WorkspaceInstance{ + WorkspaceId: id, + Status: &v1.WorkspaceInstanceStatus{ + Phase: phase, + }, + }, + }, + } + } + + type Expectation struct { + Error string + SystemException bool + } + tests := []struct { + Name string + Expectation Expectation + PrepServer func(mux *http.ServeMux) + WorkspaceID string + }{ + { + Name: "stream retry", + PrepServer: func(mux *http.ServeMux) { + mux.Handle(gitpod_experimental_v1connect.NewWorkspacesServiceHandler(&TestWorkspaceService{ + Workspaces: []*v1.Workspace{ + workspaceWithStatus("workspaceID", v1.WorkspaceInstanceStatus_PHASE_PENDING), + workspaceWithStatus("workspaceID", v1.WorkspaceInstanceStatus_PHASE_CREATING), + nil, + workspaceWithStatus("workspaceID", v1.WorkspaceInstanceStatus_PHASE_RUNNING), + }, + })) + }, + }, + { + Name: "stream ends early", + PrepServer: func(mux *http.ServeMux) { + mux.Handle(gitpod_experimental_v1connect.NewWorkspacesServiceHandler(&TestWorkspaceService{ + Workspaces: []*v1.Workspace{ + workspaceWithStatus("workspaceID", v1.WorkspaceInstanceStatus_PHASE_CREATING), + }, + })) + }, + Expectation: Expectation{ + Error: "workspace stream ended unexpectedly", + SystemException: true, + }, + }, + { + Name: "workspace starts", + PrepServer: func(mux *http.ServeMux) { + mux.Handle(gitpod_experimental_v1connect.NewWorkspacesServiceHandler(&TestWorkspaceService{ + Workspaces: []*v1.Workspace{ + workspaceWithStatus("workspaceID", v1.WorkspaceInstanceStatus_PHASE_PENDING), + workspaceWithStatus("workspaceID", v1.WorkspaceInstanceStatus_PHASE_CREATING), + workspaceWithStatus("workspaceID", v1.WorkspaceInstanceStatus_PHASE_RUNNING), + }, + })) + }, + }, + } + + for _, test := range tests { + t.Run(test.Name, func(t *testing.T) { + var act Expectation + + mux := http.NewServeMux() + if test.PrepServer != nil { + test.PrepServer(mux) + } + + apisrv := httptest.NewServer(mux) + t.Cleanup(apisrv.Close) + + clnt, err := client.New(client.WithURL(apisrv.URL), client.WithCredentials("hello world")) + if err != nil { + t.Fatal(err) + } + + _, err = ObserveWorkspaceUntilStarted(context.Background(), clnt, test.WorkspaceID) + if err != nil { + act.Error = err.Error() + _, act.SystemException = err.(*prettyprint.ErrSystemException) + } + + if diff := cmp.Diff(test.Expectation, act); diff != "" { + t.Errorf("ObserveWorkspaceUntilStarted() mismatch (-want +got):\n%s", diff) + } + }) + } +} + +type TestWorkspaceService struct { + gitpod_experimental_v1connect.WorkspacesServiceHandler + + Workspaces []*v1.Workspace + Pos int +} + +func (srv *TestWorkspaceService) GetWorkspace(context.Context, *connect.Request[v1.GetWorkspaceRequest]) (*connect.Response[v1.GetWorkspaceResponse], error) { + if srv.Pos >= len(srv.Workspaces) { + return nil, connect.NewError(connect.CodeNotFound, fmt.Errorf("not found")) + } + + resp := &connect.Response[v1.GetWorkspaceResponse]{ + Msg: &v1.GetWorkspaceResponse{ + Result: srv.Workspaces[srv.Pos], + }, + } + srv.Pos++ + return resp, nil +} + +func (srv *TestWorkspaceService) StreamWorkspaceStatus(ctx context.Context, req *connect.Request[v1.StreamWorkspaceStatusRequest], resp *connect.ServerStream[v1.StreamWorkspaceStatusResponse]) error { + if srv.Pos >= len(srv.Workspaces) { + return nil + } + if srv.Workspaces[srv.Pos] == nil { + srv.Pos++ + return nil + } + + for ; srv.Pos < len(srv.Workspaces); srv.Pos++ { + ws := srv.Workspaces[srv.Pos] + if ws == nil { + return nil + } + err := resp.Send(&v1.StreamWorkspaceStatusResponse{ + Result: ws.Status, + }) + if err != nil { + return err + } + } + return nil +}