Monorepo for Tangled tangled.org
2

Configure Feed

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

core / workflow / def_test.go
14 kB 533 lines
1package workflow 2 3import ( 4 "testing" 5 6 "github.com/stretchr/testify/assert" 7 "tangled.org/core/api/tangled" 8) 9 10func TestUnmarshalWorkflowWithBranch(t *testing.T) { 11 yamlData := ` 12when: 13 - event: ["push", "pull_request"] 14 branch: ["main", "develop"]` 15 16 wf, err := FromFile("test.yml", []byte(yamlData)) 17 assert.NoError(t, err, "YAML should unmarshal without error") 18 19 assert.Len(t, wf.When, 1, "Should have one constraint") 20 assert.ElementsMatch(t, []string{"main", "develop"}, wf.When[0].Branch) 21 assert.ElementsMatch(t, []string{"push", "pull_request"}, wf.When[0].Event) 22 23 assert.False(t, wf.CloneOpts.Skip, "Skip should default to false") 24} 25 26func TestUnmarshalWorkflowWithRunsOnLabels(t *testing.T) { 27 yamlData := ` 28engine: microvm 29runs_on: [linux/arm64, kvm] 30when: 31 - event: push` 32 33 wf, err := FromFile("test.yml", []byte(yamlData)) 34 assert.NoError(t, err) 35 36 assert.Equal(t, []string{"linux/arm64", "kvm"}, wf.RunsOn) 37} 38 39func TestUnmarshalCloneFalse(t *testing.T) { 40 yamlData := ` 41when: 42 - event: pull_request_close 43 44clone: 45 skip: true 46` 47 48 wf, err := FromFile("test.yml", []byte(yamlData)) 49 assert.NoError(t, err) 50 51 assert.ElementsMatch(t, []string{"pull_request_close"}, wf.When[0].Event) 52 53 assert.True(t, wf.CloneOpts.Skip, "Skip should be false") 54} 55 56func TestUnmarshalWorkflowWithTags(t *testing.T) { 57 yamlData := ` 58when: 59 - event: ["push"] 60 tag: ["v*", "release-*"]` 61 62 wf, err := FromFile("test.yml", []byte(yamlData)) 63 assert.NoError(t, err, "YAML should unmarshal without error") 64 65 assert.Len(t, wf.When, 1, "Should have one constraint") 66 assert.ElementsMatch(t, []string{"v*", "release-*"}, wf.When[0].Tag) 67 assert.ElementsMatch(t, []string{"push"}, wf.When[0].Event) 68} 69 70func TestUnmarshalWorkflowWithBranchAndTag(t *testing.T) { 71 yamlData := ` 72when: 73 - event: ["push"] 74 branch: ["main", "develop"] 75 tag: ["v*"]` 76 77 wf, err := FromFile("test.yml", []byte(yamlData)) 78 assert.NoError(t, err, "YAML should unmarshal without error") 79 80 assert.Len(t, wf.When, 1, "Should have one constraint") 81 assert.ElementsMatch(t, []string{"main", "develop"}, wf.When[0].Branch) 82 assert.ElementsMatch(t, []string{"v*"}, wf.When[0].Tag) 83} 84 85func TestMatchesPattern(t *testing.T) { 86 tests := []struct { 87 name string 88 input string 89 patterns []string 90 expected bool 91 }{ 92 {"exact match", "main", []string{"main"}, true}, 93 {"exact match in list", "develop", []string{"main", "develop"}, true}, 94 {"no match", "feature", []string{"main", "develop"}, false}, 95 {"wildcard prefix", "v1.0.0", []string{"v*"}, true}, 96 {"wildcard suffix", "release-1.0", []string{"*-1.0"}, true}, 97 {"wildcard middle", "feature-123-test", []string{"feature-*-test"}, true}, 98 {"double star prefix", "release-1.0.0", []string{"release-**"}, true}, 99 {"double star with slashes", "release/1.0/hotfix", []string{"release/**"}, true}, 100 {"double star matches multiple levels", "foo/bar/baz/qux", []string{"foo/**"}, true}, 101 {"double star no match", "feature/test", []string{"release/**"}, false}, 102 {"no patterns matches nothing", "anything", []string{}, false}, 103 {"pattern doesn't match", "v1.0.0", []string{"release-*"}, false}, 104 {"complex pattern", "release/v1.2.3", []string{"release/*"}, true}, 105 {"single star stops at slash", "release/1.0/hotfix", []string{"release/*"}, false}, 106 } 107 108 for _, tt := range tests { 109 t.Run(tt.name, func(t *testing.T) { 110 result, _ := matchesPattern(tt.input, tt.patterns) 111 assert.Equal(t, tt.expected, result, "matchesPattern(%q, %v) should be %v", tt.input, tt.patterns, tt.expected) 112 }) 113 } 114} 115 116func TestConstraintMatchRef_Branches(t *testing.T) { 117 tests := []struct { 118 name string 119 constraint Constraint 120 ref string 121 expected bool 122 }{ 123 { 124 name: "exact branch match", 125 constraint: Constraint{Branch: []string{"main"}}, 126 ref: "refs/heads/main", 127 expected: true, 128 }, 129 { 130 name: "branch glob match", 131 constraint: Constraint{Branch: []string{"feature-*"}}, 132 ref: "refs/heads/feature-123", 133 expected: true, 134 }, 135 { 136 name: "branch no match", 137 constraint: Constraint{Branch: []string{"main"}}, 138 ref: "refs/heads/develop", 139 expected: false, 140 }, 141 { 142 name: "no constraints matches nothing", 143 constraint: Constraint{}, 144 ref: "refs/heads/anything", 145 expected: false, 146 }, 147 } 148 149 for _, tt := range tests { 150 t.Run(tt.name, func(t *testing.T) { 151 result, _ := tt.constraint.MatchRef(tt.ref) 152 assert.Equal(t, tt.expected, result, "MatchRef should return %v for ref %q", tt.expected, tt.ref) 153 }) 154 } 155} 156 157func TestConstraintMatchRef_Tags(t *testing.T) { 158 tests := []struct { 159 name string 160 constraint Constraint 161 ref string 162 expected bool 163 }{ 164 { 165 name: "exact tag match", 166 constraint: Constraint{Tag: []string{"v1.0.0"}}, 167 ref: "refs/tags/v1.0.0", 168 expected: true, 169 }, 170 { 171 name: "tag glob match", 172 constraint: Constraint{Tag: []string{"v*"}}, 173 ref: "refs/tags/v1.2.3", 174 expected: true, 175 }, 176 { 177 name: "tag glob with pattern", 178 constraint: Constraint{Tag: []string{"release-*"}}, 179 ref: "refs/tags/release-2024", 180 expected: true, 181 }, 182 { 183 name: "tag no match", 184 constraint: Constraint{Tag: []string{"v*"}}, 185 ref: "refs/tags/release-1.0", 186 expected: false, 187 }, 188 { 189 name: "tag not matched when only branch constraint", 190 constraint: Constraint{Branch: []string{"main"}}, 191 ref: "refs/tags/v1.0.0", 192 expected: false, 193 }, 194 } 195 196 for _, tt := range tests { 197 t.Run(tt.name, func(t *testing.T) { 198 result, _ := tt.constraint.MatchRef(tt.ref) 199 assert.Equal(t, tt.expected, result, "MatchRef should return %v for ref %q", tt.expected, tt.ref) 200 }) 201 } 202} 203 204func TestConstraintMatchRef_Combined(t *testing.T) { 205 tests := []struct { 206 name string 207 constraint Constraint 208 ref string 209 expected bool 210 }{ 211 { 212 name: "matches branch in combined constraint", 213 constraint: Constraint{Branch: []string{"main"}, Tag: []string{"v*"}}, 214 ref: "refs/heads/main", 215 expected: true, 216 }, 217 { 218 name: "matches tag in combined constraint", 219 constraint: Constraint{Branch: []string{"main"}, Tag: []string{"v*"}}, 220 ref: "refs/tags/v1.0.0", 221 expected: true, 222 }, 223 { 224 name: "no match in combined constraint", 225 constraint: Constraint{Branch: []string{"main"}, Tag: []string{"v*"}}, 226 ref: "refs/heads/develop", 227 expected: false, 228 }, 229 { 230 name: "glob patterns in combined constraint - branch", 231 constraint: Constraint{Branch: []string{"release-*"}, Tag: []string{"v*"}}, 232 ref: "refs/heads/release-2024", 233 expected: true, 234 }, 235 { 236 name: "glob patterns in combined constraint - tag", 237 constraint: Constraint{Branch: []string{"release-*"}, Tag: []string{"v*"}}, 238 ref: "refs/tags/v2.0.0", 239 expected: true, 240 }, 241 } 242 243 for _, tt := range tests { 244 t.Run(tt.name, func(t *testing.T) { 245 result, _ := tt.constraint.MatchRef(tt.ref) 246 assert.Equal(t, tt.expected, result, "MatchRef should return %v for ref %q", tt.expected, tt.ref) 247 }) 248 } 249} 250 251func TestConstraintMatchBranch_GlobPatterns(t *testing.T) { 252 tests := []struct { 253 name string 254 constraint Constraint 255 branch string 256 expected bool 257 }{ 258 { 259 name: "exact match", 260 constraint: Constraint{Branch: []string{"main"}}, 261 branch: "main", 262 expected: true, 263 }, 264 { 265 name: "glob match", 266 constraint: Constraint{Branch: []string{"feature-*"}}, 267 branch: "feature-123", 268 expected: true, 269 }, 270 { 271 name: "no match", 272 constraint: Constraint{Branch: []string{"main"}}, 273 branch: "develop", 274 expected: false, 275 }, 276 { 277 name: "multiple patterns with match", 278 constraint: Constraint{Branch: []string{"main", "release-*"}}, 279 branch: "release-1.0", 280 expected: true, 281 }, 282 } 283 284 for _, tt := range tests { 285 t.Run(tt.name, func(t *testing.T) { 286 result, _ := tt.constraint.MatchBranch(tt.branch) 287 assert.Equal(t, tt.expected, result, "MatchBranch should return %v for branch %q", tt.expected, tt.branch) 288 }) 289 } 290} 291 292func TestMatchesAnyFile(t *testing.T) { 293 tests := []struct { 294 name string 295 files []string 296 patterns []string 297 expected bool 298 }{ 299 { 300 name: "exact file match", 301 files: []string{"src/main.go"}, 302 patterns: []string{"src/main.go"}, 303 expected: true, 304 }, 305 { 306 name: "glob match single star", 307 files: []string{"src/main.go"}, 308 patterns: []string{"src/*.go"}, 309 expected: true, 310 }, 311 { 312 name: "glob match double star", 313 files: []string{"src/pkg/util.go"}, 314 patterns: []string{"src/**/*.go"}, 315 expected: true, 316 }, 317 { 318 name: "any file in list matches", 319 files: []string{"README.md", "src/main.go", "docs/guide.md"}, 320 patterns: []string{"src/**"}, 321 expected: true, 322 }, 323 { 324 name: "no file matches", 325 files: []string{"README.md", "docs/guide.md"}, 326 patterns: []string{"src/**"}, 327 expected: false, 328 }, 329 { 330 name: "empty files list", 331 files: []string{}, 332 patterns: []string{"src/**"}, 333 expected: false, 334 }, 335 { 336 name: "nil files list", 337 files: nil, 338 patterns: []string{"src/**"}, 339 expected: false, 340 }, 341 { 342 name: "multiple patterns, second matches", 343 files: []string{"docs/guide.md"}, 344 patterns: []string{"src/**", "docs/**"}, 345 expected: true, 346 }, 347 { 348 name: "single star does not cross directory boundary", 349 files: []string{"src/pkg/util.go"}, 350 patterns: []string{"src/*.go"}, 351 expected: false, 352 }, 353 } 354 355 for _, tt := range tests { 356 t.Run(tt.name, func(t *testing.T) { 357 result, err := matchesAnyFile(tt.files, tt.patterns) 358 assert.NoError(t, err) 359 assert.Equal(t, tt.expected, result) 360 }) 361 } 362} 363 364func TestConstraintMatch_PathsFilter(t *testing.T) { 365 pushTrigger := tangled.Pipeline_TriggerMetadata{ 366 Kind: string(TriggerKindPush), 367 Push: &tangled.Pipeline_PushTriggerData{ 368 Ref: "refs/heads/main", 369 }, 370 } 371 372 tests := []struct { 373 name string 374 constraint Constraint 375 changedFiles []string 376 expected bool 377 }{ 378 { 379 name: "paths match - workflow runs", 380 constraint: Constraint{ 381 Event: []string{"push"}, 382 Branch: []string{"main"}, 383 Paths: []string{"src/**"}, 384 }, 385 changedFiles: []string{"src/main.go"}, 386 expected: true, 387 }, 388 { 389 name: "paths no match - workflow skipped", 390 constraint: Constraint{ 391 Event: []string{"push"}, 392 Branch: []string{"main"}, 393 Paths: []string{"src/**"}, 394 }, 395 changedFiles: []string{"docs/guide.md"}, 396 expected: false, 397 }, 398 { 399 name: "no paths filter - all files pass", 400 constraint: Constraint{ 401 Event: []string{"push"}, 402 Branch: []string{"main"}, 403 }, 404 changedFiles: []string{"docs/guide.md"}, 405 expected: true, 406 }, 407 { 408 name: "paths filter with empty changed files - skipped", 409 constraint: Constraint{ 410 Event: []string{"push"}, 411 Branch: []string{"main"}, 412 Paths: []string{"src/**"}, 413 }, 414 changedFiles: []string{}, 415 expected: false, 416 }, 417 { 418 name: "paths glob matches one of many changed files", 419 constraint: Constraint{ 420 Event: []string{"push"}, 421 Branch: []string{"main"}, 422 Paths: []string{"**/*.go"}, 423 }, 424 changedFiles: []string{"README.md", "go.mod", "src/main.go"}, 425 expected: true, 426 }, 427 } 428 429 for _, tt := range tests { 430 t.Run(tt.name, func(t *testing.T) { 431 result, err := tt.constraint.Match(pushTrigger, tt.changedFiles) 432 assert.NoError(t, err) 433 assert.Equal(t, tt.expected, result) 434 }) 435 } 436} 437 438func TestUnmarshalWorkflowWithPaths(t *testing.T) { 439 yamlData := ` 440when: 441 - event: push 442 branch: main 443 paths: 444 - "src/**" 445 - "**.go"` 446 447 wf, err := FromFile("test.yml", []byte(yamlData)) 448 assert.NoError(t, err) 449 assert.Len(t, wf.When, 1) 450 assert.ElementsMatch(t, []string{"src/**", "**.go"}, wf.When[0].Paths) 451} 452 453func TestUnmarshalWorkflowWithPathsSingleString(t *testing.T) { 454 yamlData := ` 455when: 456 - event: push 457 branch: main 458 paths: "src/**"` 459 460 wf, err := FromFile("test.yml", []byte(yamlData)) 461 assert.NoError(t, err) 462 assert.Len(t, wf.When, 1) 463 assert.ElementsMatch(t, []string{"src/**"}, wf.When[0].Paths) 464} 465 466func TestConstraintMatchTag_GlobPatterns(t *testing.T) { 467 tests := []struct { 468 name string 469 constraint Constraint 470 tag string 471 expected bool 472 }{ 473 { 474 name: "exact match", 475 constraint: Constraint{Tag: []string{"v1.0.0"}}, 476 tag: "v1.0.0", 477 expected: true, 478 }, 479 { 480 name: "glob match", 481 constraint: Constraint{Tag: []string{"v*"}}, 482 tag: "v2.3.4", 483 expected: true, 484 }, 485 { 486 name: "no match", 487 constraint: Constraint{Tag: []string{"v*"}}, 488 tag: "release-1.0", 489 expected: false, 490 }, 491 { 492 name: "multiple patterns with match", 493 constraint: Constraint{Tag: []string{"v*", "release-*"}}, 494 tag: "release-2024", 495 expected: true, 496 }, 497 { 498 name: "empty tag list matches nothing", 499 constraint: Constraint{Tag: []string{}}, 500 tag: "v1.0.0", 501 expected: false, 502 }, 503 } 504 505 for _, tt := range tests { 506 t.Run(tt.name, func(t *testing.T) { 507 result, _ := tt.constraint.MatchTag(tt.tag) 508 assert.Equal(t, tt.expected, result, "MatchTag should return %v for tag %q", tt.expected, tt.tag) 509 }) 510 } 511} 512 513func TestMatch_ManualDispatch(t *testing.T) { 514 // manual dispatch is policy-free: every workflow matches regardless of its 515 // declared event/branch/tag/path constraints. Selection is the caller's job. 516 manualTrigger := tangled.Pipeline_TriggerMetadata{ 517 Kind: string(TriggerKindManual), 518 Manual: &tangled.Pipeline_ManualTriggerData{Sha: "deadbeef"}, 519 } 520 521 workflows := []Workflow{ 522 {When: nil}, 523 {When: []Constraint{{Event: []string{"push"}, Branch: []string{"main"}}}}, 524 {When: []Constraint{{Event: []string{"pull_request"}, Paths: []string{"src/**"}}}}, 525 {When: []Constraint{{Event: []string{"push"}, Tag: []string{"v*"}}}}, 526 } 527 528 for i, wf := range workflows { 529 result, err := wf.Match(manualTrigger, nil) 530 assert.NoError(t, err) 531 assert.True(t, result, "workflow %d should match a manual dispatch", i) 532 } 533}