this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

test(controller): add more test with Temporal SDK

+689
+269
controller/activities/activity_test.go
··· 1 + package activities 2 + 3 + import ( 4 + "context" 5 + "strings" 6 + "testing" 7 + "time" 8 + 9 + "github.com/stretchr/testify/assert" 10 + "github.com/stretchr/testify/suite" 11 + "go.temporal.io/sdk/testsuite" 12 + ) 13 + 14 + type ActivityTestSuite struct { 15 + suite.Suite 16 + testsuite.WorkflowTestSuite 17 + 18 + env *testsuite.TestActivityEnvironment 19 + } 20 + 21 + func (s *ActivityTestSuite) SetupTest() { 22 + s.env = s.NewTestActivityEnvironment() 23 + s.env.SetTestTimeout(30 * time.Second) 24 + } 25 + 26 + // Test graph pruning logic with direct calls 27 + func (s *ActivityTestSuite) TestPruneGraph_Success() { 28 + ctx := context.Background() 29 + originalGraph := &Graph{ 30 + Nodes: map[string]bool{ 31 + "vpc": true, 32 + "database": true, 33 + "app": true, 34 + }, 35 + Edges: map[string][]string{ 36 + "database": {"vpc"}, 37 + "app": {"database"}, 38 + }, 39 + } 40 + changedFiles := []string{"database"} 41 + 42 + prunedGraph, err := PruneGraph(ctx, originalGraph, changedFiles) 43 + 44 + s.NoError(err) 45 + s.Equal(2, prunedGraph.NodeCount()) // database and app (which depends on database) 46 + s.Equal(1, prunedGraph.EdgeCount()) // app -> database 47 + s.True(prunedGraph.Nodes["database"]) 48 + s.True(prunedGraph.Nodes["app"]) 49 + s.False(prunedGraph.Nodes["vpc"]) // vpc should be pruned as it's not changed and no dependents 50 + } 51 + 52 + func (s *ActivityTestSuite) TestPruneGraph_EmptyChanges() { 53 + ctx := context.Background() 54 + originalGraph := &Graph{ 55 + Nodes: map[string]bool{ 56 + "vpc": true, 57 + "database": true, 58 + }, 59 + Edges: map[string][]string{ 60 + "database": {"vpc"}, 61 + }, 62 + } 63 + changedFiles := []string{} // No changes 64 + 65 + prunedGraph, err := PruneGraph(ctx, originalGraph, changedFiles) 66 + 67 + s.NoError(err) 68 + s.Equal(0, prunedGraph.NodeCount()) 69 + s.Equal(0, prunedGraph.EdgeCount()) 70 + } 71 + 72 + func (s *ActivityTestSuite) TestPruneGraph_ComplexDependencies() { 73 + ctx := context.Background() 74 + // Complex graph: monitoring -> app -> [database, cache] -> vpc 75 + originalGraph := &Graph{ 76 + Nodes: map[string]bool{ 77 + "vpc": true, 78 + "database": true, 79 + "cache": true, 80 + "app": true, 81 + "monitoring": true, 82 + }, 83 + Edges: map[string][]string{ 84 + "database": {"vpc"}, 85 + "cache": {"vpc"}, 86 + "app": {"database", "cache"}, 87 + "monitoring": {"app"}, 88 + }, 89 + } 90 + changedFiles := []string{"database"} // Only database changed 91 + 92 + prunedGraph, err := PruneGraph(ctx, originalGraph, changedFiles) 93 + 94 + s.NoError(err) 95 + // Should include: database (changed), app (depends on database), monitoring (depends on app) 96 + s.Equal(3, prunedGraph.NodeCount()) 97 + s.True(prunedGraph.Nodes["database"]) 98 + s.True(prunedGraph.Nodes["app"]) 99 + s.True(prunedGraph.Nodes["monitoring"]) 100 + s.False(prunedGraph.Nodes["vpc"]) // not a dependent 101 + s.False(prunedGraph.Nodes["cache"]) // not a dependent 102 + } 103 + 104 + // Test using TestActivityEnvironment for activities that need proper context 105 + func (s *ActivityTestSuite) TestTerragruntPrune_WithActivityEnvironment() { 106 + originalGraph := &Graph{ 107 + Nodes: map[string]bool{ 108 + "vpc": true, 109 + "database": true, 110 + "app": true, 111 + }, 112 + Edges: map[string][]string{ 113 + "database": {"vpc"}, 114 + "app": {"database"}, 115 + }, 116 + } 117 + changedFiles := []string{"database"} 118 + 119 + // Register the activity first 120 + s.env.RegisterActivity(TerragruntPrune) 121 + 122 + // Use the activity environment to execute the activity 123 + val, err := s.env.ExecuteActivity(TerragruntPrune, originalGraph, changedFiles) 124 + s.NoError(err) 125 + 126 + var result *Graph 127 + err = val.Get(&result) 128 + s.NoError(err) 129 + s.Equal(2, result.NodeCount()) 130 + s.Equal(1, result.EdgeCount()) 131 + } 132 + 133 + func TestActivityTestSuite(t *testing.T) { 134 + suite.Run(t, new(ActivityTestSuite)) 135 + } 136 + 137 + // Additional comprehensive unit tests for graph functions 138 + func TestNewGraphFromDot_EmptyGraph(t *testing.T) { 139 + dotString := `digraph { 140 + }` 141 + 142 + graph, err := NewGraphFromDot(dotString) 143 + 144 + assert.NoError(t, err) 145 + assert.Equal(t, 0, graph.NodeCount()) 146 + assert.Equal(t, 0, graph.EdgeCount()) 147 + } 148 + 149 + func TestNewGraphFromDot_InvalidFormat(t *testing.T) { 150 + dotString := `not a valid dot format` 151 + 152 + graph, err := NewGraphFromDot(dotString) 153 + 154 + assert.NoError(t, err) // Should not error, just ignore invalid lines 155 + assert.Equal(t, 0, graph.NodeCount()) 156 + } 157 + 158 + func TestGraph_TopologicalSort_CyclicGraph(t *testing.T) { 159 + // Create a graph with a cycle: A -> B -> C -> A 160 + graph := &Graph{ 161 + Nodes: map[string]bool{ 162 + "a": true, 163 + "b": true, 164 + "c": true, 165 + }, 166 + Edges: map[string][]string{ 167 + "a": {"b"}, 168 + "b": {"c"}, 169 + "c": {"a"}, // Creates cycle 170 + }, 171 + } 172 + 173 + levels := graph.TopologicalSort() 174 + 175 + // Should handle cycles gracefully by putting remaining nodes in final level 176 + assert.Greater(t, len(levels), 0) 177 + 178 + // All nodes should be present somewhere 179 + allNodes := make(map[string]bool) 180 + for _, level := range levels { 181 + for _, node := range level { 182 + allNodes[node] = true 183 + } 184 + } 185 + assert.Len(t, allNodes, 3) 186 + assert.True(t, allNodes["a"]) 187 + assert.True(t, allNodes["b"]) 188 + assert.True(t, allNodes["c"]) 189 + } 190 + 191 + func TestExtractQuotedString(t *testing.T) { 192 + tests := []struct { 193 + input string 194 + expected string 195 + }{ 196 + {`"hello"`, "hello"}, 197 + {`"hello world"`, "hello world"}, 198 + {`""`, ""}, 199 + {`hello`, ""}, // No quotes 200 + {`"hello`, ""}, // Missing closing quote 201 + {`hello"`, ""}, // Missing opening quote 202 + {` "hello" `, "hello"}, // With whitespace 203 + } 204 + 205 + for _, test := range tests { 206 + result := extractQuotedString(test.input) 207 + assert.Equal(t, test.expected, result, "Input: %s", test.input) 208 + } 209 + } 210 + 211 + func TestGraph_AddEdge_CreatesNodes(t *testing.T) { 212 + graph := NewGraph() 213 + 214 + graph.AddEdge("a", "b") 215 + 216 + assert.Equal(t, 2, graph.NodeCount()) 217 + assert.Equal(t, 1, graph.EdgeCount()) 218 + assert.True(t, graph.Nodes["a"]) 219 + assert.True(t, graph.Nodes["b"]) 220 + assert.Contains(t, graph.Edges["a"], "b") 221 + } 222 + 223 + func TestGraph_GetNodes(t *testing.T) { 224 + graph := &Graph{ 225 + Nodes: map[string]bool{ 226 + "vpc": true, 227 + "database": true, 228 + "app": true, 229 + }, 230 + Edges: map[string][]string{}, 231 + } 232 + 233 + nodes := graph.GetNodes() 234 + 235 + assert.Len(t, nodes, 3) 236 + assert.Contains(t, nodes, "vpc") 237 + assert.Contains(t, nodes, "database") 238 + assert.Contains(t, nodes, "app") 239 + } 240 + 241 + func TestClone_PathGeneration(t *testing.T) { 242 + // Test that generateRepoPath creates deterministic paths 243 + url1 := "https://github.com/example/repo.git" 244 + revision1 := "main" 245 + 246 + path1 := generateRepoPath(url1, revision1) 247 + path2 := generateRepoPath(url1, revision1) 248 + 249 + // Same inputs should generate same path 250 + assert.Equal(t, path1, path2) 251 + 252 + // Different inputs should generate different paths 253 + path3 := generateRepoPath(url1, "develop") 254 + assert.NotEqual(t, path1, path3) 255 + 256 + path4 := generateRepoPath("https://github.com/other/repo.git", revision1) 257 + assert.NotEqual(t, path1, path4) 258 + 259 + // Paths should be under /tmp/cloudlab-repos/ 260 + assert.True(t, strings.HasPrefix(path1, "/tmp/cloudlab-repos/")) 261 + } 262 + 263 + func TestClone_CheckRepoStatus(t *testing.T) { 264 + // Test checkRepoStatus with non-existent directory 265 + nonExistentPath := "/tmp/non-existent-repo-12345" 266 + exists, hash := checkRepoStatus(context.Background(), nonExistentPath, "main") 267 + assert.False(t, exists) 268 + assert.Empty(t, hash) 269 + }
+420
controller/workflows/infra_test.go
··· 1 + package workflows 2 + 3 + import ( 4 + "context" 5 + "errors" 6 + "testing" 7 + "time" 8 + 9 + "cloudlab/controller/activities" 10 + 11 + "github.com/stretchr/testify/mock" 12 + "github.com/stretchr/testify/suite" 13 + "go.temporal.io/sdk/testsuite" 14 + ) 15 + 16 + type InfraWorkflowTestSuite struct { 17 + suite.Suite 18 + testsuite.WorkflowTestSuite 19 + 20 + env *testsuite.TestWorkflowEnvironment 21 + } 22 + 23 + func (s *InfraWorkflowTestSuite) SetupTest() { 24 + s.env = s.NewTestWorkflowEnvironment() 25 + // Set a reasonable timeout for tests 26 + s.env.SetTestTimeout(30 * time.Second) 27 + } 28 + 29 + func (s *InfraWorkflowTestSuite) AfterTest(suiteName, testName string) { 30 + s.env.AssertExpectations(s.T()) 31 + } 32 + 33 + func (s *InfraWorkflowTestSuite) TestInfraWorkflow_Success() { 34 + // Mock data 35 + input := InfraInputs{ 36 + Url: "https://github.com/example/repo.git", 37 + Revision: "main", 38 + OldRevision: "HEAD~1", 39 + Stack: "dev", 40 + } 41 + repoPath := "/tmp/infra-12345" 42 + changedModules := []string{"module1", "module2"} 43 + 44 + // Create a sample graph 45 + graph := &activities.Graph{ 46 + Nodes: map[string]bool{ 47 + "module1": true, 48 + "module2": true, 49 + "module3": true, 50 + }, 51 + Edges: map[string][]string{ 52 + "module1": {"module2"}, // module1 depends on module2 53 + }, 54 + } 55 + 56 + // Create pruned graph (only changed modules and dependents) 57 + prunedGraph := &activities.Graph{ 58 + Nodes: map[string]bool{ 59 + "module1": true, 60 + "module2": true, 61 + }, 62 + Edges: map[string][]string{ 63 + "module1": {"module2"}, 64 + }, 65 + } 66 + 67 + // Mock activities - use mock.Anything for context parameter 68 + s.env.OnActivity(activities.Clone, mock.Anything, input.Url, input.Revision).Return(repoPath, nil) 69 + s.env.OnActivity(activities.TerragruntGraph, mock.Anything, repoPath+"/infra/"+input.Stack).Return(graph, nil) 70 + s.env.OnActivity(activities.ChangedModules, mock.Anything, repoPath, input.OldRevision).Return(changedModules, nil) 71 + s.env.OnActivity(activities.TerragruntPrune, mock.Anything, graph, changedModules).Return(prunedGraph, nil) 72 + s.env.OnActivity(activities.TerragruntApply, mock.Anything, input.Url, input.Revision, "module2", input.Stack).Return(nil) 73 + s.env.OnActivity(activities.TerragruntApply, mock.Anything, input.Url, input.Revision, "module1", input.Stack).Return(nil) 74 + 75 + // Execute workflow 76 + s.env.ExecuteWorkflow(Infra, input) 77 + 78 + // Assertions 79 + s.True(s.env.IsWorkflowCompleted()) 80 + s.NoError(s.env.GetWorkflowError()) 81 + 82 + var result *activities.Graph 83 + s.NoError(s.env.GetWorkflowResult(&result)) 84 + s.Equal(prunedGraph, result) 85 + } 86 + 87 + func (s *InfraWorkflowTestSuite) TestInfraWorkflow_CloneFailure() { 88 + input := InfraInputs{ 89 + Url: "https://github.com/example/invalid-repo.git", 90 + Revision: "main", 91 + OldRevision: "HEAD~1", 92 + Stack: "dev", 93 + } 94 + 95 + // Mock Clone to return error 96 + s.env.OnActivity(activities.Clone, mock.Anything, input.Url, input.Revision).Return("", errors.New("repository not found")) 97 + 98 + s.env.ExecuteWorkflow(Infra, input) 99 + 100 + s.True(s.env.IsWorkflowCompleted()) 101 + s.Error(s.env.GetWorkflowError()) 102 + s.Contains(s.env.GetWorkflowError().Error(), "repository not found") 103 + } 104 + 105 + func (s *InfraWorkflowTestSuite) TestInfraWorkflow_TerragruntGraphFailure() { 106 + input := InfraInputs{ 107 + Url: "https://github.com/example/repo.git", 108 + Revision: "main", 109 + OldRevision: "HEAD~1", 110 + Stack: "dev", 111 + } 112 + repoPath := "/tmp/infra-12345" 113 + 114 + s.env.OnActivity(activities.Clone, mock.Anything, input.Url, input.Revision).Return(repoPath, nil) 115 + s.env.OnActivity(activities.TerragruntGraph, mock.Anything, repoPath+"/infra/"+input.Stack).Return( 116 + (*activities.Graph)(nil), errors.New("terragrunt dag graph failed")) 117 + 118 + s.env.ExecuteWorkflow(Infra, input) 119 + 120 + s.True(s.env.IsWorkflowCompleted()) 121 + s.Error(s.env.GetWorkflowError()) 122 + s.Contains(s.env.GetWorkflowError().Error(), "terragrunt dag graph failed") 123 + } 124 + 125 + func (s *InfraWorkflowTestSuite) TestInfraWorkflow_ChangedModulesFailure() { 126 + input := InfraInputs{ 127 + Url: "https://github.com/example/repo.git", 128 + Revision: "main", 129 + OldRevision: "HEAD~1", 130 + Stack: "dev", 131 + } 132 + repoPath := "/tmp/infra-12345" 133 + graph := &activities.Graph{ 134 + Nodes: map[string]bool{"module1": true}, 135 + Edges: map[string][]string{}, 136 + } 137 + 138 + s.env.OnActivity(activities.Clone, mock.Anything, input.Url, input.Revision).Return(repoPath, nil) 139 + s.env.OnActivity(activities.TerragruntGraph, mock.Anything, repoPath+"/infra/"+input.Stack).Return(graph, nil) 140 + s.env.OnActivity(activities.ChangedModules, mock.Anything, repoPath, input.OldRevision).Return( 141 + []string{}, errors.New("git diff failed")) 142 + 143 + s.env.ExecuteWorkflow(Infra, input) 144 + 145 + s.True(s.env.IsWorkflowCompleted()) 146 + s.Error(s.env.GetWorkflowError()) 147 + s.Contains(s.env.GetWorkflowError().Error(), "git diff failed") 148 + } 149 + 150 + func (s *InfraWorkflowTestSuite) TestInfraWorkflow_TerragruntApplyFailure() { 151 + input := InfraInputs{ 152 + Url: "https://github.com/example/repo.git", 153 + Revision: "main", 154 + OldRevision: "HEAD~1", 155 + Stack: "dev", 156 + } 157 + repoPath := "/tmp/infra-12345" 158 + changedModules := []string{"module1"} 159 + 160 + graph := &activities.Graph{ 161 + Nodes: map[string]bool{"module1": true}, 162 + Edges: map[string][]string{}, 163 + } 164 + 165 + prunedGraph := &activities.Graph{ 166 + Nodes: map[string]bool{"module1": true}, 167 + Edges: map[string][]string{}, 168 + } 169 + 170 + s.env.OnActivity(activities.Clone, mock.Anything, input.Url, input.Revision).Return(repoPath, nil) 171 + s.env.OnActivity(activities.TerragruntGraph, mock.Anything, repoPath+"/infra/"+input.Stack).Return(graph, nil) 172 + s.env.OnActivity(activities.ChangedModules, mock.Anything, repoPath, input.OldRevision).Return(changedModules, nil) 173 + s.env.OnActivity(activities.TerragruntPrune, mock.Anything, graph, changedModules).Return(prunedGraph, nil) 174 + s.env.OnActivity(activities.TerragruntApply, mock.Anything, input.Url, input.Revision, "module1", input.Stack).Return( 175 + errors.New("terragrunt apply failed: resource conflict")) 176 + 177 + s.env.ExecuteWorkflow(Infra, input) 178 + 179 + s.True(s.env.IsWorkflowCompleted()) 180 + s.Error(s.env.GetWorkflowError()) 181 + s.Contains(s.env.GetWorkflowError().Error(), "terragrunt apply failed") 182 + } 183 + 184 + func (s *InfraWorkflowTestSuite) TestInfraWorkflow_ComplexDependencyGraph() { 185 + input := InfraInputs{ 186 + Url: "https://github.com/example/repo.git", 187 + Revision: "main", 188 + OldRevision: "HEAD~1", 189 + Stack: "prod", 190 + } 191 + repoPath := "/tmp/infra-67890" 192 + changedModules := []string{"vpc", "database"} 193 + 194 + // Complex dependency graph: 195 + // app -> [database, loadbalancer] 196 + // database -> vpc 197 + // loadbalancer -> vpc 198 + // monitoring -> app 199 + graph := &activities.Graph{ 200 + Nodes: map[string]bool{ 201 + "vpc": true, 202 + "database": true, 203 + "loadbalancer": true, 204 + "app": true, 205 + "monitoring": true, 206 + }, 207 + Edges: map[string][]string{ 208 + "app": {"database", "loadbalancer"}, 209 + "database": {"vpc"}, 210 + "loadbalancer": {"vpc"}, 211 + "monitoring": {"app"}, 212 + }, 213 + } 214 + 215 + // Pruned graph should contain changed modules and their dependents 216 + prunedGraph := &activities.Graph{ 217 + Nodes: map[string]bool{ 218 + "vpc": true, 219 + "database": true, 220 + "app": true, 221 + "monitoring": true, 222 + }, 223 + Edges: map[string][]string{ 224 + "app": {"database"}, 225 + "database": {"vpc"}, 226 + "monitoring": {"app"}, 227 + }, 228 + } 229 + 230 + s.env.OnActivity(activities.Clone, mock.Anything, input.Url, input.Revision).Return(repoPath, nil) 231 + s.env.OnActivity(activities.TerragruntGraph, mock.Anything, repoPath+"/infra/"+input.Stack).Return(graph, nil) 232 + s.env.OnActivity(activities.ChangedModules, mock.Anything, repoPath, input.OldRevision).Return(changedModules, nil) 233 + s.env.OnActivity(activities.TerragruntPrune, mock.Anything, graph, changedModules).Return(prunedGraph, nil) 234 + 235 + // Mock TerragruntApply calls in dependency order 236 + // Level 0: vpc 237 + s.env.OnActivity(activities.TerragruntApply, mock.Anything, input.Url, input.Revision, "vpc", input.Stack).Return(nil) 238 + // Level 1: database 239 + s.env.OnActivity(activities.TerragruntApply, mock.Anything, input.Url, input.Revision, "database", input.Stack).Return(nil) 240 + // Level 2: app 241 + s.env.OnActivity(activities.TerragruntApply, mock.Anything, input.Url, input.Revision, "app", input.Stack).Return(nil) 242 + // Level 3: monitoring 243 + s.env.OnActivity(activities.TerragruntApply, mock.Anything, input.Url, input.Revision, "monitoring", input.Stack).Return(nil) 244 + 245 + s.env.ExecuteWorkflow(Infra, input) 246 + 247 + s.True(s.env.IsWorkflowCompleted()) 248 + s.NoError(s.env.GetWorkflowError()) 249 + 250 + var result *activities.Graph 251 + s.NoError(s.env.GetWorkflowResult(&result)) 252 + s.Equal(4, result.NodeCount()) 253 + s.Equal(3, result.EdgeCount()) 254 + } 255 + 256 + func (s *InfraWorkflowTestSuite) TestInfraWorkflow_NoChangedModules() { 257 + input := InfraInputs{ 258 + Url: "https://github.com/example/repo.git", 259 + Revision: "main", 260 + OldRevision: "HEAD~1", 261 + Stack: "dev", 262 + } 263 + repoPath := "/tmp/infra-12345" 264 + changedModules := []string{} // No changes 265 + 266 + graph := &activities.Graph{ 267 + Nodes: map[string]bool{ 268 + "module1": true, 269 + "module2": true, 270 + }, 271 + Edges: map[string][]string{ 272 + "module1": {"module2"}, 273 + }, 274 + } 275 + 276 + // Pruned graph should be empty 277 + prunedGraph := &activities.Graph{ 278 + Nodes: map[string]bool{}, 279 + Edges: map[string][]string{}, 280 + } 281 + 282 + s.env.OnActivity(activities.Clone, mock.Anything, input.Url, input.Revision).Return(repoPath, nil) 283 + s.env.OnActivity(activities.TerragruntGraph, mock.Anything, repoPath+"/infra/"+input.Stack).Return(graph, nil) 284 + s.env.OnActivity(activities.ChangedModules, mock.Anything, repoPath, input.OldRevision).Return(changedModules, nil) 285 + s.env.OnActivity(activities.TerragruntPrune, mock.Anything, graph, changedModules).Return(prunedGraph, nil) 286 + 287 + // No TerragruntApply calls should be made since no modules to deploy 288 + 289 + s.env.ExecuteWorkflow(Infra, input) 290 + 291 + s.True(s.env.IsWorkflowCompleted()) 292 + s.NoError(s.env.GetWorkflowError()) 293 + 294 + var result *activities.Graph 295 + s.NoError(s.env.GetWorkflowResult(&result)) 296 + s.Equal(0, result.NodeCount()) 297 + s.Equal(0, result.EdgeCount()) 298 + } 299 + 300 + func (s *InfraWorkflowTestSuite) TestInfraWorkflow_ActivityTimeout() { 301 + input := InfraInputs{ 302 + Url: "https://github.com/example/repo.git", 303 + Revision: "main", 304 + OldRevision: "HEAD~1", 305 + Stack: "dev", 306 + } 307 + 308 + // Mock Clone to simulate a timeout scenario 309 + s.env.OnActivity(activities.Clone, mock.Anything, input.Url, input.Revision).Return( 310 + "", errors.New("activity timeout")) 311 + 312 + s.env.ExecuteWorkflow(Infra, input) 313 + 314 + s.True(s.env.IsWorkflowCompleted()) 315 + s.Error(s.env.GetWorkflowError()) 316 + } 317 + 318 + func (s *InfraWorkflowTestSuite) TestInfraWorkflow_ParallelExecution() { 319 + // Test that modules at the same dependency level are executed in parallel 320 + input := InfraInputs{ 321 + Url: "https://github.com/example/repo.git", 322 + Revision: "main", 323 + OldRevision: "HEAD~1", 324 + Stack: "dev", 325 + } 326 + repoPath := "/tmp/infra-12345" 327 + changedModules := []string{"module-a", "module-b", "module-c"} 328 + 329 + // Graph with parallel modules: 330 + // module-a and module-b can run in parallel (both depend on module-c) 331 + graph := &activities.Graph{ 332 + Nodes: map[string]bool{ 333 + "module-a": true, 334 + "module-b": true, 335 + "module-c": true, 336 + }, 337 + Edges: map[string][]string{ 338 + "module-a": {"module-c"}, 339 + "module-b": {"module-c"}, 340 + }, 341 + } 342 + 343 + prunedGraph := graph // All modules changed 344 + 345 + s.env.OnActivity(activities.Clone, mock.Anything, input.Url, input.Revision).Return(repoPath, nil) 346 + s.env.OnActivity(activities.TerragruntGraph, mock.Anything, repoPath+"/infra/"+input.Stack).Return(graph, nil) 347 + s.env.OnActivity(activities.ChangedModules, mock.Anything, repoPath, input.OldRevision).Return(changedModules, nil) 348 + s.env.OnActivity(activities.TerragruntPrune, mock.Anything, graph, changedModules).Return(prunedGraph, nil) 349 + 350 + // Level 0: module-c 351 + s.env.OnActivity(activities.TerragruntApply, mock.Anything, input.Url, input.Revision, "module-c", input.Stack).Return(nil) 352 + // Level 1: module-a and module-b (should execute in parallel) 353 + s.env.OnActivity(activities.TerragruntApply, mock.Anything, input.Url, input.Revision, "module-a", input.Stack).Return(nil) 354 + s.env.OnActivity(activities.TerragruntApply, mock.Anything, input.Url, input.Revision, "module-b", input.Stack).Return(nil) 355 + 356 + s.env.ExecuteWorkflow(Infra, input) 357 + 358 + s.True(s.env.IsWorkflowCompleted()) 359 + s.NoError(s.env.GetWorkflowError()) 360 + } 361 + 362 + func (s *InfraWorkflowTestSuite) TestInfraWorkflow_WorkerFailureRetry() { 363 + // Test that TerragruntApply can handle worker failure and retry on a different worker 364 + // by ensuring the activity is self-contained (clones repo internally) 365 + input := InfraInputs{ 366 + Url: "https://github.com/example/repo.git", 367 + Revision: "main", 368 + OldRevision: "HEAD~1", 369 + Stack: "dev", 370 + } 371 + repoPath := "/tmp/infra-12345" 372 + newWorkerRepoPath := "/tmp/infra-67890" 373 + changedModules := []string{"module1"} 374 + 375 + graph := &activities.Graph{ 376 + Nodes: map[string]bool{ 377 + "module1": true, 378 + }, 379 + Edges: map[string][]string{}, 380 + } 381 + 382 + prunedGraph := graph // Only module1 changed 383 + 384 + // Initial workflow activities (successful) 385 + s.env.OnActivity(activities.Clone, mock.Anything, input.Url, input.Revision).Return(repoPath, nil) 386 + s.env.OnActivity(activities.TerragruntGraph, mock.Anything, repoPath+"/infra/"+input.Stack).Return(graph, nil) 387 + s.env.OnActivity(activities.ChangedModules, mock.Anything, repoPath, input.OldRevision).Return(changedModules, nil) 388 + s.env.OnActivity(activities.TerragruntPrune, mock.Anything, graph, changedModules).Return(prunedGraph, nil) 389 + 390 + // Simulate worker failure and retry on different worker 391 + applyCallCount := 0 392 + s.env.OnActivity(activities.TerragruntApply, mock.Anything, input.Url, input.Revision, "module1", input.Stack).Return( 393 + func(ctx context.Context, repoUrl, revision, modulePath, stack string) error { 394 + applyCallCount++ 395 + if applyCallCount == 1 { 396 + // First attempt fails (simulating worker failure) 397 + return errors.New("worker failed: connection lost") 398 + } 399 + // Second attempt succeeds (activity is self-contained and clones repo again) 400 + return nil 401 + }) 402 + 403 + // Mock additional Clone calls for TerragruntApply retries 404 + // The activity will call Clone internally to ensure repo availability 405 + s.env.OnActivity(activities.Clone, mock.Anything, input.Url, input.Revision).Return(newWorkerRepoPath, nil).Maybe() 406 + 407 + s.env.ExecuteWorkflow(Infra, input) 408 + 409 + s.True(s.env.IsWorkflowCompleted()) 410 + s.NoError(s.env.GetWorkflowError()) 411 + 412 + var result *activities.Graph 413 + s.NoError(s.env.GetWorkflowResult(&result)) 414 + s.Equal(1, result.NodeCount()) 415 + s.Equal(0, result.EdgeCount()) 416 + } 417 + 418 + func TestInfraWorkflowTestSuite(t *testing.T) { 419 + suite.Run(t, new(InfraWorkflowTestSuite)) 420 + }