cloudflare/pint

Public

mirrored from https://github.com/cloudflare/pintAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v0.49.1

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

internal/discovery/git_blame_test.go

499lines · modecode

1package discovery_test
2
3import (
4 "fmt"
5 "os"
6 "path"
7 "regexp"
8 "strconv"
9 "strings"
10 "testing"
11
12 "github.com/stretchr/testify/require"
13
14 "github.com/cloudflare/pint/internal/discovery"
15)
16
17func blameLine(sha string, line int, filename string) string {
18 return fmt.Sprintf(`%s %d %d 1
19author Alice Mock
20author-mail <alice@example.com>
21author-time 1559927997
22author-tz 0000
23committer Alice Mock
24committer-mail <alice@example.com>
25committer-time 1559927997
26committer-tz 0000
27summary Mock commit title
28boundary
29filename %s
30 fake content
31`, sha, line, line, filename)
32}
33
34type blameRange struct {
35 sha string
36 lines []int
37}
38
39func blame(v map[string][]blameRange) []byte {
40 var out string
41 for path, brs := range v {
42 for _, br := range brs {
43 for _, l := range br.lines {
44 out += blameLine(br.sha, l, path)
45 }
46 }
47 }
48 return []byte(out)
49}
50
51type rule struct {
52 path string
53 name string
54 lines []int
55 modified []int
56}
57
58func TestGitBlameFinder(t *testing.T) {
59 type testCaseT struct {
60 files map[string]string
61 finder discovery.GitBlameFinder
62 rules []rule
63 err string
64 }
65
66 testRuleBody := `
67- record: first
68 expr: sum(foo)
69
70- alert: second
71 expr: foo > bar
72 labels:
73 cluster: dev
74
75- record: third
76 expr: count(foo)
77 labels:
78 cluster: dev
79`
80
81 testCases := []testCaseT{
82 {
83 files: map[string]string{},
84 finder: discovery.NewGitBlameFinder(
85 func(args ...string) ([]byte, error) {
86 return nil, fmt.Errorf("mock error")
87 },
88 nil,
89 nil,
90 "main",
91 0,
92 nil,
93 ),
94 err: "failed to get the list of commits to scan: mock error",
95 },
96 {
97 files: map[string]string{},
98 finder: discovery.NewGitBlameFinder(
99 func(args ...string) ([]byte, error) {
100 switch strings.Join(args, " ") {
101 case "log --format=%H --no-abbrev-commit --reverse main..HEAD":
102 return []byte("commit1\n"), nil
103 default:
104 return nil, fmt.Errorf("mock error")
105 }
106 },
107 nil,
108 nil,
109 "main",
110 0,
111 nil,
112 ),
113 err: "failed to get the list of modified files from git: mock error",
114 },
115 {
116 files: map[string]string{},
117 finder: discovery.NewGitBlameFinder(
118 func(args ...string) ([]byte, error) {
119 switch strings.Join(args, " ") {
120 case "log --format=%H --no-abbrev-commit --reverse main..HEAD":
121 return []byte("commit1\n"), nil
122 case "log --reverse --no-merges --pretty=format:%H --name-status commit1^..commit1":
123 return []byte("commit1\nM foo.yml\n"), nil
124 case "show -s --format=%B commit1":
125 return nil, fmt.Errorf("mock error")
126 default:
127 t.Errorf("unknown args: %v", args)
128 t.FailNow()
129 return nil, nil
130 }
131 },
132 nil,
133 nil,
134 "main",
135 0,
136 []*regexp.Regexp{regexp.MustCompile(".*")},
137 ),
138 err: "failed to get commit message for commit1: mock error",
139 },
140 {
141 files: map[string]string{},
142 finder: discovery.NewGitBlameFinder(
143 func(args ...string) ([]byte, error) {
144 switch strings.Join(args, " ") {
145 case "log --format=%H --no-abbrev-commit --reverse main..HEAD":
146 return []byte("commit1\n"), nil
147 case "log --reverse --no-merges --pretty=format:%H --name-status commit1^..commit1":
148 return []byte("commit1\nM foo.yml\n"), nil
149 case "show -s --format=%B commit1":
150 return []byte("foo"), nil
151 case "blame --line-porcelain -- foo.yml":
152 return nil, fmt.Errorf("mock error")
153 default:
154 t.Errorf("unknown args: %v", args)
155 t.FailNow()
156 return nil, nil
157 }
158 },
159 nil,
160 nil,
161 "main",
162 0,
163 []*regexp.Regexp{regexp.MustCompile(".*")},
164 ),
165 err: "failed to run git blame for foo.yml: mock error",
166 },
167 {
168 files: map[string]string{},
169 finder: discovery.NewGitBlameFinder(
170 func(args ...string) ([]byte, error) {
171 switch strings.Join(args, " ") {
172 case "log --format=%H --no-abbrev-commit --reverse main..HEAD":
173 return []byte("commit1\n"), nil
174 case "log --reverse --no-merges --pretty=format:%H --name-status commit1^..commit1":
175 return []byte("commit1\nM foo.yml\n"), nil
176 case "show -s --format=%B commit1":
177 return []byte("foo"), nil
178 case "blame --line-porcelain -- foo.yml":
179 return blame(map[string][]blameRange{
180 "foo.yml": {
181 {sha: "commitX", lines: []int{1, 3, 4, 5, 6, 9, 10, 11, 12}},
182 {sha: "commit1", lines: []int{2, 7, 8}},
183 },
184 }), nil
185 default:
186 t.Errorf("unknown args: %v", args)
187 t.FailNow()
188 return nil, nil
189 }
190 },
191 nil,
192 nil,
193 "main",
194 0,
195 []*regexp.Regexp{regexp.MustCompile(".*")},
196 ),
197 err: "open foo.yml: no such file or directory",
198 },
199 {
200 files: map[string]string{
201 "foo.yml": testRuleBody,
202 },
203 finder: discovery.NewGitBlameFinder(
204 func(args ...string) ([]byte, error) {
205 switch strings.Join(args, " ") {
206 case "log --format=%H --no-abbrev-commit --reverse main..HEAD":
207 return []byte("commit1\n"), nil
208 case "log --reverse --no-merges --pretty=format:%H --name-status commit1^..commit1":
209 return []byte("commit1\nM foo.yml\n"), nil
210 case "show -s --format=%B commit1":
211 return []byte("foo"), nil
212 case "blame --line-porcelain -- foo.yml":
213 return blame(map[string][]blameRange{
214 "foo.yml": {
215 {sha: "commitX", lines: []int{1, 3, 4, 5, 6, 9, 10, 11, 12}},
216 {sha: "commit1", lines: []int{2, 7, 8}},
217 },
218 }), nil
219 default:
220 t.Errorf("unknown args: %v", args)
221 t.FailNow()
222 return nil, nil
223 }
224 },
225 nil,
226 nil,
227 "main",
228 0,
229 []*regexp.Regexp{regexp.MustCompile(".*")},
230 ),
231 rules: []rule{
232 {path: "foo.yml", name: "first", lines: []int{2, 3}, modified: []int{2}},
233 {path: "foo.yml", name: "second", lines: []int{5, 6, 7, 8}, modified: []int{7, 8}},
234 },
235 },
236 {
237 files: map[string]string{
238 "foo/i1.yml": testRuleBody,
239 "i2.yml": testRuleBody,
240 "foo/c1a.yml": testRuleBody,
241 "foo/c1b.yml": testRuleBody,
242 "c2a.yml": testRuleBody,
243 "c2c.yml": testRuleBody,
244 "foo/c2b.yml": testRuleBody,
245 "bar/c3a.yml": testRuleBody,
246 "c3b.yml": testRuleBody,
247 "c3c.yml": testRuleBody,
248 "c3d.yml": testRuleBody,
249 "c3e.yml": testRuleBody,
250 },
251 finder: discovery.NewGitBlameFinder(
252 func(args ...string) ([]byte, error) {
253 switch strings.Join(args, " ") {
254 case "log --format=%H --no-abbrev-commit --reverse main..HEAD":
255 return []byte("commit1\ncommit2\ncommit3\n"), nil
256 case "log --reverse --no-merges --pretty=format:%H --name-status commit1^..commit3":
257 return []byte(`commit1
258M foo/c1a.yml
259M foo/c1b.yml
260commit2
261M c2a.yml
262M foo/c2b.yml
263A foo/c2c.yml
264commit3
265
266A bar/c3a.yml
267R053 src.txt c3b.yml
268R100 foo/c3c.txt c3c.yml
269M c2a.yml
270C50 foo/cp1.yml c3d.yml
271D foo/c2b.yml
272R090 foo/c2c.yml c2c.yml
273`), nil
274 case "show -s --format=%B commit1":
275 return []byte("foo"), nil
276 case "show -s --format=%B commit2":
277 return []byte("bar"), nil
278 case "show -s --format=%B commit3":
279 return []byte("foo"), nil
280 case "blame --line-porcelain -- foo/c1a.yml":
281 return blame(map[string][]blameRange{
282 "foo/c1a.yml": {
283 {sha: "commit1", lines: []int{2, 12}}, // 1 & 3
284 {sha: "commitX", lines: []int{1, 3, 4, 5, 6, 7, 8, 9, 10, 11}},
285 },
286 }), nil
287 case "blame --line-porcelain -- foo/c1b.yml":
288 return blame(map[string][]blameRange{
289 "foo/c1b.yml": {
290 {sha: "commit1", lines: []int{11, 12}}, // 3
291 {sha: "commitX", lines: []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}},
292 },
293 }), nil
294 case "blame --line-porcelain -- c2a.yml":
295 return blame(map[string][]blameRange{
296 "c2a.yml": {
297 {sha: "commitX", lines: []int{1, 2, 3, 4, 5, 6, 9, 11, 12}},
298 {sha: "commit2", lines: []int{7, 8, 10}}, // 2 & 3
299 {sha: "commit3", lines: []int{3}}, // 1
300 },
301 }), nil
302 case "blame --line-porcelain -- c2c.yml":
303 return blame(map[string][]blameRange{
304 "c2c.yml": {
305 {sha: "commit2", lines: []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}}, // 2 & 3
306 {sha: "commit3", lines: []int{3}}, // 1
307 },
308 }), nil
309 case "blame --line-porcelain -- c3b.yml":
310 return blame(map[string][]blameRange{
311 "c3b.yml": {
312 {sha: "commitX", lines: []int{1, 11, 12}},
313 {sha: "commit3", lines: []int{2, 3, 4, 5, 6, 7, 8, 9, 10}}, // 1 & 2 & 3
314 },
315 }), nil
316 case "blame --line-porcelain -- c3c.yml":
317 return blame(map[string][]blameRange{
318 "c3c.yml": {
319 {sha: "commit3", lines: []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}}, // 1 & 2 & 3
320 },
321 }), nil
322 case "blame --line-porcelain -- c3d.yml":
323 return blame(map[string][]blameRange{
324 "c3d.yml": {
325 {sha: "commitX", lines: []int{1, 11, 12}},
326 {sha: "commit3", lines: []int{2, 3, 4, 5, 6, 7, 8, 9, 10}}, // 1 & 2 & 3
327 },
328 }), nil
329 default:
330 t.Errorf("unknown args: %v", args)
331 t.FailNow()
332 return nil, nil
333 }
334 },
335 []*regexp.Regexp{
336 regexp.MustCompile("^foo/.*"),
337 regexp.MustCompile("^c.*.yml$"),
338 },
339 nil,
340 "main",
341 0,
342 []*regexp.Regexp{regexp.MustCompile(".*")},
343 ),
344 rules: []rule{
345 {path: "c3b.yml", name: "first", lines: []int{2, 3}, modified: []int{2, 3}},
346 {path: "c3b.yml", name: "second", lines: []int{5, 6, 7, 8}, modified: []int{5, 6, 7, 8}},
347 {path: "c3b.yml", name: "third", lines: []int{10, 11, 12, 13}, modified: []int{10}},
348 {path: "c3c.yml", name: "first", lines: []int{2, 3}, modified: []int{2, 3}},
349 {path: "c3c.yml", name: "second", lines: []int{5, 6, 7, 8}, modified: []int{5, 6, 7, 8}},
350 {path: "c3c.yml", name: "third", lines: []int{10, 11, 12, 13}, modified: []int{10, 11, 12}},
351 {path: "c3d.yml", name: "first", lines: []int{2, 3}, modified: []int{2, 3}},
352 {path: "c3d.yml", name: "second", lines: []int{5, 6, 7, 8}, modified: []int{5, 6, 7, 8}},
353 {path: "c3d.yml", name: "third", lines: []int{10, 11, 12, 13}, modified: []int{10}},
354 {path: "foo/c1a.yml", name: "first", lines: []int{2, 3}, modified: []int{2}},
355 {path: "foo/c1a.yml", name: "third", lines: []int{10, 11, 12, 13}, modified: []int{12}},
356 {path: "foo/c1b.yml", name: "third", lines: []int{10, 11, 12, 13}, modified: []int{11, 12}},
357 {path: "c2a.yml", name: "first", lines: []int{2, 3}, modified: []int{3}},
358 {path: "c2a.yml", name: "second", lines: []int{5, 6, 7, 8}, modified: []int{7, 8}},
359 {path: "c2a.yml", name: "third", lines: []int{10, 11, 12, 13}, modified: []int{10}},
360 {path: "c2c.yml", name: "first", lines: []int{2, 3}, modified: []int{2, 3, 3}},
361 {path: "c2c.yml", name: "second", lines: []int{5, 6, 7, 8}, modified: []int{5, 6, 7, 8}},
362 {path: "c2c.yml", name: "third", lines: []int{10, 11, 12, 13}, modified: []int{10, 11, 12}},
363 },
364 },
365 {
366 files: map[string]string{
367 "foo.yml": testRuleBody,
368 },
369 finder: discovery.NewGitBlameFinder(
370 func(args ...string) ([]byte, error) {
371 switch strings.Join(args, " ") {
372 case "log --format=%H --no-abbrev-commit --reverse main..HEAD":
373 return []byte("commit1\n"), nil
374 case "log --reverse --no-merges --pretty=format:%H --name-status commit1^..commit1":
375 return []byte("commit1\nM foo.yml\n"), nil
376 case "show -s --format=%B commit1":
377 return []byte("foo"), nil
378 case "blame --line-porcelain -- foo.yml":
379 return blame(map[string][]blameRange{
380 "foo.yml": {
381 {sha: "commitX", lines: []int{1, 3, 4, 5, 6, 9, 10, 11, 12}},
382 {sha: "commit1", lines: []int{2, 7, 8}},
383 },
384 }), nil
385 default:
386 t.Errorf("unknown args: %v", args)
387 t.FailNow()
388 return nil, nil
389 }
390 },
391 nil,
392 nil,
393 "main",
394 0,
395 nil,
396 ),
397 rules: []rule{
398 {path: "foo.yml", modified: []int{2, 7, 8}},
399 },
400 },
401 {
402 files: map[string]string{
403 "foo.yml": testRuleBody,
404 },
405 finder: discovery.NewGitBlameFinder(
406 func(args ...string) ([]byte, error) {
407 switch strings.Join(args, " ") {
408 case "log --format=%H --no-abbrev-commit --reverse main..HEAD":
409 return []byte("commit1\n"), nil
410 case "log --reverse --no-merges --pretty=format:%H --name-status commit1^..commit1":
411 return []byte("commit1\nM foo.yml\n"), nil
412 case "show -s --format=%B commit1":
413 return []byte("foo [skip ci] bar"), nil
414 default:
415 t.Errorf("unknown args: %v", args)
416 t.FailNow()
417 return nil, nil
418 }
419 },
420 nil,
421 nil,
422 "main",
423 0,
424 []*regexp.Regexp{regexp.MustCompile(".*")},
425 ),
426 rules: nil,
427 },
428 {
429 files: map[string]string{
430 "foo.yml": testRuleBody,
431 },
432 finder: discovery.NewGitBlameFinder(
433 func(args ...string) ([]byte, error) {
434 switch strings.Join(args, " ") {
435 case "log --format=%H --no-abbrev-commit --reverse main..HEAD":
436 return []byte("commit1\n"), nil
437 case "log --reverse --no-merges --pretty=format:%H --name-status commit1^..commit1":
438 return []byte("commit1\nM foo.yml\n"), nil
439 case "show -s --format=%B commit1":
440 return []byte("foo [no ci] bar"), nil
441 default:
442 t.Errorf("unknown args: %v", args)
443 t.FailNow()
444 return nil, nil
445 }
446 },
447 nil,
448 nil,
449 "main",
450 0,
451 []*regexp.Regexp{regexp.MustCompile(".*")},
452 ),
453 rules: nil,
454 },
455 }
456
457 for i, tc := range testCases {
458 t.Run(strconv.Itoa(i), func(t *testing.T) {
459 workdir := t.TempDir()
460 err := os.Chdir(workdir)
461 require.NoError(t, err)
462
463 for p, content := range tc.files {
464 if strings.Contains(p, "/") {
465 err = os.MkdirAll(path.Dir(p), 0o755)
466 require.NoError(t, err)
467 }
468 err = os.WriteFile(p, []byte(content), 0o644)
469 require.NoError(t, err)
470 }
471
472 entries, err := tc.finder.Find()
473 if tc.err != "" {
474 require.EqualError(t, err, tc.err)
475 } else {
476 require.NoError(t, err)
477
478 rules := []rule{}
479 for _, e := range entries {
480 t.Logf("Entry: path=%s pathErr=%v lines=%v modified=%v", e.SourcePath, e.PathError, e.Rule.Lines(), e.ModifiedLines)
481 var name string
482 if e.Rule.AlertingRule != nil {
483 name = e.Rule.AlertingRule.Alert.Value.Value
484 }
485 if e.Rule.RecordingRule != nil {
486 name = e.Rule.RecordingRule.Record.Value.Value
487 }
488 rules = append(rules, rule{
489 path: e.SourcePath,
490 name: name,
491 lines: e.Rule.Lines(),
492 modified: e.ModifiedLines,
493 })
494 }
495 require.ElementsMatch(t, tc.rules, rules)
496 }
497 })
498 }
499}
500