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