Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cli/cmd/smoketest_codesphere.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ var availableSteps = []teststeps.SmokeTestStep{
&teststeps.SetEnvVarStep{},
&teststeps.CreateFilesStep{},
&teststeps.SyncLandscapeStep{},
&teststeps.StartPipelineStep{},
&teststeps.ExecuteRunStageStep{},
&teststeps.DeleteWorkspaceStep{},
}

Expand Down
262 changes: 247 additions & 15 deletions cli/cmd/smoketest_codesphere_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import (
func mockFullTestRun(mockClient *codesphere.MockClient, teamId, planId, workspaceId int) {
// Expect the rest of the steps to run with the fetched plan ID
mockClient.EXPECT().CreateWorkspace(
teamId, // teamID
teamId,
planId, // fetched planID
mock.AnythingOfType("string"), // workspace name is timestamped
(*string)(nil), // empty workspace
Expand Down Expand Up @@ -60,6 +60,13 @@ func mockFullTestRun(mockClient *codesphere.MockClient, teamId, planId, workspac
"run",
).Return(nil).Once()

mockClient.EXPECT().GetPipelineState(
workspaceId,
"run",
).Return([]api.PipelineStatus{
{State: "running", Server: "customer-server", Replica: "replica-0"},
}, nil).Once()

mockClient.EXPECT().DeleteWorkspace(
workspaceId,
).Return(nil).Once()
Expand Down Expand Up @@ -180,8 +187,8 @@ var _ = Describe("SmoketestCodesphereCmd", func() {

It("deletes workspace even on CreateWorkspace failure", func() {
mockClient.EXPECT().CreateWorkspace(
teamIdInt, // teamID
planIdInt, // planID
teamIdInt,
planIdInt,
mock.AnythingOfType("string"),
(*string)(nil), // empty workspace
).Return(0, fmt.Errorf("create failed")).Once()
Expand All @@ -194,8 +201,8 @@ var _ = Describe("SmoketestCodesphereCmd", func() {
workspaceID := 789

mockClient.EXPECT().CreateWorkspace(
teamIdInt, // teamID
planIdInt, // planID
teamIdInt,
planIdInt,
mock.AnythingOfType("string"),
(*string)(nil), // empty workspace
).Return(workspaceID, nil).Once()
Expand All @@ -216,8 +223,8 @@ var _ = Describe("SmoketestCodesphereCmd", func() {

It("deletes workspace on ExecuteCommand failure", func() {
mockClient.EXPECT().CreateWorkspace(
teamIdInt, // teamID
planIdInt, // planID
teamIdInt,
planIdInt,
mock.AnythingOfType("string"),
(*string)(nil), // empty workspace
).Return(workspaceId, nil).Once()
Expand Down Expand Up @@ -246,8 +253,8 @@ var _ = Describe("SmoketestCodesphereCmd", func() {

It("deletes workspace on SyncLandscape failure", func() {
mockClient.EXPECT().CreateWorkspace(
teamIdInt, // teamID
planIdInt, // planID
teamIdInt,
planIdInt,
mock.AnythingOfType("string"),
(*string)(nil), // empty workspace
).Return(workspaceId, nil).Once()
Expand Down Expand Up @@ -289,8 +296,8 @@ var _ = Describe("SmoketestCodesphereCmd", func() {

It("deletes workspace on StartPipeline failure", func() {
mockClient.EXPECT().CreateWorkspace(
teamIdInt, // teamID
planIdInt, // planID
teamIdInt,
planIdInt,
mock.AnythingOfType("string"),
(*string)(nil), // empty workspace
).Return(workspaceId, nil).Once()
Expand Down Expand Up @@ -336,10 +343,228 @@ var _ = Describe("SmoketestCodesphereCmd", func() {
Expect(err).To(MatchError(ContainSubstring("failed to start pipeline")))
})

It("deletes workspace when run stage reaches failure state", func() {
mockClient.EXPECT().CreateWorkspace(
teamIdInt,
planIdInt,
mock.AnythingOfType("string"),
(*string)(nil), // empty workspace
).Return(workspaceId, nil).Once()

mockClient.EXPECT().SetEnvVar(
workspaceId,
"TEST_VAR",
"smoketest",
).Return(nil).Once()

mockClient.EXPECT().ExecuteCommand(
workspaceId,
mock.MatchedBy(func(cmd string) bool {
return strings.Contains(cmd, "> ci.yml")
}),
).Return(nil).Once()

mockClient.EXPECT().ExecuteCommand(
workspaceId,
mock.MatchedBy(func(cmd string) bool {
return strings.Contains(cmd, "> index.html")
}),
).Return(nil).Once()

mockClient.EXPECT().SyncLandscape(
workspaceId,
"ci.yml",
).Return(nil).Once()

mockClient.EXPECT().StartPipeline(
workspaceId,
"ci.yml",
"run",
).Return(nil).Once()

mockClient.EXPECT().GetPipelineState(
workspaceId,
"run",
).Return([]api.PipelineStatus{
{State: "failure", Server: "customer-server", Replica: "replica-0"},
}, nil).Once()

mockClient.EXPECT().DeleteWorkspace(
workspaceId,
).Return(nil).Once()

err := c.RunSmoketest()
Expect(err).To(MatchError(ContainSubstring("unexpected state")))
})

It("deletes workspace when run stage reaches aborted state", func() {
mockClient.EXPECT().CreateWorkspace(
teamIdInt,
planIdInt,
mock.AnythingOfType("string"),
(*string)(nil), // empty workspace
).Return(workspaceId, nil).Once()

mockClient.EXPECT().SetEnvVar(
workspaceId,
"TEST_VAR",
"smoketest",
).Return(nil).Once()

mockClient.EXPECT().ExecuteCommand(
workspaceId,
mock.MatchedBy(func(cmd string) bool {
return strings.Contains(cmd, "> ci.yml")
}),
).Return(nil).Once()

mockClient.EXPECT().ExecuteCommand(
workspaceId,
mock.MatchedBy(func(cmd string) bool {
return strings.Contains(cmd, "> index.html")
}),
).Return(nil).Once()

mockClient.EXPECT().SyncLandscape(
workspaceId,
"ci.yml",
).Return(nil).Once()

mockClient.EXPECT().StartPipeline(
workspaceId,
"ci.yml",
"run",
).Return(nil).Once()

mockClient.EXPECT().GetPipelineState(
workspaceId,
"run",
).Return([]api.PipelineStatus{
{State: "aborted", Server: "customer-server", Replica: "replica-0"},
}, nil).Once()

mockClient.EXPECT().DeleteWorkspace(
workspaceId,
).Return(nil).Once()

err := c.RunSmoketest()
Expect(err).To(MatchError(ContainSubstring("unexpected state")))
})

It("retries GetPipelineState on error and times out including the last error", func() {
mockClient.EXPECT().CreateWorkspace(
teamIdInt,
planIdInt,
mock.AnythingOfType("string"),
(*string)(nil), // empty workspace
).Return(workspaceId, nil).Once()

mockClient.EXPECT().SetEnvVar(
workspaceId,
"TEST_VAR",
"smoketest",
).Return(nil).Once()

mockClient.EXPECT().ExecuteCommand(
workspaceId,
mock.MatchedBy(func(cmd string) bool {
return strings.Contains(cmd, "> ci.yml")
}),
).Return(nil).Once()

mockClient.EXPECT().ExecuteCommand(
workspaceId,
mock.MatchedBy(func(cmd string) bool {
return strings.Contains(cmd, "> index.html")
}),
).Return(nil).Once()

mockClient.EXPECT().SyncLandscape(
workspaceId,
"ci.yml",
).Return(nil).Once()

mockClient.EXPECT().StartPipeline(
workspaceId,
"ci.yml",
"run",
).Return(nil).Once()

mockClient.EXPECT().GetPipelineState(
workspaceId,
"run",
).Return(nil, fmt.Errorf("connection refused")).Once()

mockClient.EXPECT().DeleteWorkspace(
workspaceId,
).Return(nil).Once()

opts.Timeout = 100 * time.Millisecond
err := c.RunSmoketest()
Expect(err).To(MatchError(ContainSubstring("timed out")))
Expect(err).To(MatchError(ContainSubstring("connection refused")))
})

It("does not return success when only IDE server is in running state", func() {
mockClient.EXPECT().CreateWorkspace(
teamIdInt,
planIdInt,
mock.AnythingOfType("string"),
(*string)(nil),
).Return(workspaceId, nil).Once()

mockClient.EXPECT().SetEnvVar(
workspaceId,
"TEST_VAR",
"smoketest",
).Return(nil).Once()

mockClient.EXPECT().ExecuteCommand(
workspaceId,
mock.MatchedBy(func(cmd string) bool {
return strings.Contains(cmd, "> ci.yml")
}),
).Return(nil).Once()

mockClient.EXPECT().ExecuteCommand(
workspaceId,
mock.MatchedBy(func(cmd string) bool {
return strings.Contains(cmd, "> index.html")
}),
).Return(nil).Once()

mockClient.EXPECT().SyncLandscape(
workspaceId,
"ci.yml",
).Return(nil).Once()

mockClient.EXPECT().StartPipeline(
workspaceId,
"ci.yml",
"run",
).Return(nil).Once()

// Only IDE server running — must NOT be treated as success
mockClient.EXPECT().GetPipelineState(
workspaceId,
"run",
).Return([]api.PipelineStatus{
{State: "running", Server: "codesphere-ide", Replica: "replica-0"},
}, nil).Once()

mockClient.EXPECT().DeleteWorkspace(
workspaceId,
).Return(nil).Once()

opts.Timeout = 100 * time.Millisecond
err := c.RunSmoketest()
Expect(err).To(MatchError(ContainSubstring("timed out")))
})

It("returns cleanup error when DeleteWorkspace fails", func() {
mockClient.EXPECT().CreateWorkspace(
teamIdInt, // teamID
planIdInt, // planID
teamIdInt,
planIdInt,
mock.AnythingOfType("string"),
(*string)(nil), // empty workspace
).Return(workspaceId, nil).Once()
Expand Down Expand Up @@ -377,6 +602,13 @@ var _ = Describe("SmoketestCodesphereCmd", func() {
"run",
).Return(nil).Once()

mockClient.EXPECT().GetPipelineState(
workspaceId,
"run",
).Return([]api.PipelineStatus{
{State: "running", Server: "customer-server", Replica: "replica-0"},
}, nil).Once()

mockClient.EXPECT().DeleteWorkspace(
workspaceId,
).Return(fmt.Errorf("delete failed")).Once()
Expand All @@ -389,8 +621,8 @@ var _ = Describe("SmoketestCodesphereCmd", func() {
opts.Steps = []string{"createWorkspace", "setEnvVar"}

mockClient.EXPECT().CreateWorkspace(
teamIdInt, // teamID
planIdInt, // planID
teamIdInt,
planIdInt,
mock.AnythingOfType("string"),
(*string)(nil),
).Return(workspaceId, nil).Once()
Expand Down
2 changes: 1 addition & 1 deletion docs/oms_smoketest_codesphere.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ $ oms smoketest codesphere --baseurl https://codesphere.example.com/api --token
--plan-id string Plan ID for workspace creation
--profile string CI profile to use for landscape and pipeline (default "ci.yml")
-q, --quiet Suppress progress logging
--steps strings Comma-separated list of steps to run (createWorkspace,setEnvVar,createFiles,syncLandscape,startPipeline,deleteWorkspace). If empty, all steps including deleteWorkspace are run. If specified without deleteWorkspace, the workspace will be kept for manual inspection.
--steps strings Comma-separated list of steps to run (createWorkspace,setEnvVar,createFiles,syncLandscape,executeRunStage,deleteWorkspace). If empty, all steps including deleteWorkspace are run. If specified without deleteWorkspace, the workspace will be kept for manual inspection.
--team-id string Team ID for workspace creation
--timeout duration Timeout for the entire smoke test (default 10m0s)
--token string API token for authentication
Expand Down
10 changes: 10 additions & 0 deletions internal/codesphere/codesphere.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ type Client interface {
ExecuteCommand(workspaceID int, command string) error
SyncLandscape(workspaceID int, profile string) error
StartPipeline(workspaceID int, profile, stage string) error
GetPipelineState(workspaceID int, stage string) ([]api.PipelineStatus, error)
DeleteWorkspace(workspaceID int) error
ListWorkspacePlans() ([]api.WorkspacePlan, error)
ListTeams() ([]api.Team, error)
Expand Down Expand Up @@ -108,6 +109,15 @@ func (c *APIClient) StartPipeline(workspaceID int, profile, stage string) error
return nil
}

// GetPipelineState returns the current state of a pipeline stage
func (c *APIClient) GetPipelineState(workspaceID int, stage string) ([]api.PipelineStatus, error) {
states, err := c.client.GetPipelineState(workspaceID, stage)
if err != nil {
return nil, fmt.Errorf("failed to get pipeline state: %w", err)
}
return states, nil
}

// DeleteWorkspace deletes a workspace
func (c *APIClient) DeleteWorkspace(workspaceID int) error {
err := c.client.DeleteWorkspace(workspaceID)
Expand Down
Loading
Loading