Skip to content

Commit b9a8b4d

Browse files
committed
Add test for codeowners and cmd to find tests in go project without owners
1 parent c362fc4 commit b9a8b4d

File tree

4 files changed

+194
-9
lines changed

4 files changed

+194
-9
lines changed
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
"log"
6+
"path/filepath"
7+
8+
"github.com/smartcontractkit/chainlink-testing-framework/tools/flakeguard/codeowners"
9+
"github.com/smartcontractkit/chainlink-testing-framework/tools/flakeguard/reports"
10+
"github.com/spf13/cobra"
11+
)
12+
13+
var (
14+
codeownersPath string
15+
printTestFunctions bool
16+
)
17+
18+
// CheckTestOwnersCmd checks which tests lack code owners
19+
var CheckTestOwnersCmd = &cobra.Command{
20+
Use: "check-test-owners",
21+
Short: "Check which tests in the project do not have code owners",
22+
RunE: func(cmd *cobra.Command, args []string) error {
23+
// Scan project for test functions
24+
testFileMap, err := reports.ScanTestFiles(projectPath)
25+
if err != nil {
26+
log.Fatalf("Error scanning test files: %v", err)
27+
}
28+
29+
// Parse CODEOWNERS file
30+
codeOwnerPatterns, err := codeowners.Parse(codeownersPath)
31+
if err != nil {
32+
log.Fatalf("Error parsing CODEOWNERS file: %v", err)
33+
}
34+
35+
// Check for tests without code owners
36+
testsWithoutOwners := make(map[string]string) // Map of test names to their relative paths
37+
for testName, filePath := range testFileMap {
38+
relFilePath, err := filepath.Rel(projectPath, filePath)
39+
if err != nil {
40+
fmt.Printf("Error getting relative path for test %s: %v\n", testName, err)
41+
continue
42+
}
43+
// Convert to Unix-style path for matching
44+
relFilePath = filepath.ToSlash(relFilePath)
45+
46+
owners := codeowners.FindOwners(relFilePath, codeOwnerPatterns)
47+
if len(owners) == 0 {
48+
testsWithoutOwners[testName] = relFilePath
49+
}
50+
}
51+
52+
// Calculate percentages
53+
totalTests := len(testFileMap)
54+
totalWithoutOwners := len(testsWithoutOwners)
55+
totalWithOwners := totalTests - totalWithoutOwners
56+
57+
percentageWithOwners := float64(totalWithOwners) / float64(totalTests) * 100
58+
percentageWithoutOwners := float64(totalWithoutOwners) / float64(totalTests) * 100
59+
60+
// Report results
61+
fmt.Printf("Total Test functions found: %d\n", totalTests)
62+
fmt.Printf("Test functions with owners: %d (%.2f%%)\n", totalWithOwners, percentageWithOwners)
63+
fmt.Printf("Test functions without owners: %d (%.2f%%)\n", totalWithoutOwners, percentageWithoutOwners)
64+
65+
if printTestFunctions {
66+
67+
if totalWithoutOwners > 0 {
68+
fmt.Println("\nTest functions without owners:")
69+
for testName, relPath := range testsWithoutOwners {
70+
fmt.Printf("- %s (%s)\n", testName, relPath)
71+
}
72+
} else {
73+
fmt.Println("All Test functions have code owners!")
74+
}
75+
}
76+
77+
return nil
78+
},
79+
}
80+
81+
func init() {
82+
CheckTestOwnersCmd.Flags().StringVarP(&projectPath, "project-path", "p", ".", "Path to the root of the project")
83+
CheckTestOwnersCmd.Flags().StringVarP(&codeownersPath, "codeowners-path", "c", ".github/CODEOWNERS", "Path to the CODEOWNERS file")
84+
CheckTestOwnersCmd.Flags().BoolVarP(&printTestFunctions, "print-test-functions", "t", false, "Print all test functions without owners")
85+
}

tools/flakeguard/codeowners/parser.go

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ package codeowners
22

33
import (
44
"bufio"
5+
"fmt"
56
"os"
7+
"path"
68
"path/filepath"
79
"regexp"
810
"strings"
@@ -35,29 +37,49 @@ func Parse(filePath string) ([]PatternOwner, error) {
3537
fields := strings.Fields(line)
3638
if len(fields) > 1 {
3739
patterns = append(patterns, PatternOwner{
38-
Pattern: fields[0],
40+
Pattern: filepath.ToSlash(fields[0]), // Normalize to Unix-style
3941
Owners: fields[1:],
4042
})
4143
}
4244
}
4345
return patterns, scanner.Err()
4446
}
4547

46-
// FindOwners determines the owners for a given file path based on patterns
48+
func IsWildcardPattern(pattern string) bool {
49+
return strings.ContainsAny(pattern, "*?[")
50+
}
51+
52+
// FindOwners finds the owners of a file based on the CODEOWNERS patterns
4753
func FindOwners(filePath string, patterns []PatternOwner) []string {
48-
// Convert filePath to Unix-style for matching
54+
// Normalize the file path to Unix-style
4955
relFilePath := filepath.ToSlash(filePath)
5056

5157
var matchedOwners []string
5258
for _, pattern := range patterns {
53-
// Ensure the pattern is also converted to Unix-style
54-
patternPath := strings.TrimPrefix(pattern.Pattern, "/")
55-
patternPath = strings.TrimSuffix(patternPath, "/")
59+
// Normalize the pattern to Unix-style and remove leading and trailing slashes
60+
normalizedPattern := filepath.ToSlash(strings.TrimPrefix(pattern.Pattern, "/"))
61+
normalizedPattern = strings.TrimSuffix(normalizedPattern, "/")
5662

57-
// Match if the file is in the directory or is an exact match
58-
if strings.HasPrefix(relFilePath, patternPath) || relFilePath == patternPath {
59-
matchedOwners = pattern.Owners
63+
if IsWildcardPattern(normalizedPattern) {
64+
matched, err := path.Match(normalizedPattern, relFilePath)
65+
if err != nil {
66+
fmt.Printf("Error matching pattern: %s to file: %s, error: %v\n", normalizedPattern, relFilePath, err)
67+
continue
68+
}
69+
70+
if matched {
71+
matchedOwners = pattern.Owners
72+
}
73+
} else {
74+
if relFilePath == normalizedPattern {
75+
// Exact file or directory match
76+
matchedOwners = pattern.Owners
77+
} else if strings.HasPrefix(relFilePath, normalizedPattern+"/") {
78+
// File is under the directory pattern
79+
matchedOwners = pattern.Owners
80+
}
6081
}
6182
}
83+
6284
return matchedOwners
6385
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package codeowners
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
)
8+
9+
func TestFindOwners(t *testing.T) {
10+
tests := []struct {
11+
name string
12+
filePath string
13+
patterns []PatternOwner
14+
expectedOwners []string
15+
}{
16+
{
17+
name: "Exact match",
18+
filePath: "core/services/job/job_test.go",
19+
patterns: []PatternOwner{
20+
{Pattern: "/core/services/job", Owners: []string{"@team1", "@team2"}}, // Leading /
21+
},
22+
expectedOwners: []string{"@team1", "@team2"},
23+
},
24+
{
25+
name: "Wildcard match",
26+
filePath: "core/services/ocr_test.go",
27+
patterns: []PatternOwner{
28+
{Pattern: "/core/services/ocr*", Owners: []string{"@ocr-team"}}, // Leading /
29+
},
30+
expectedOwners: []string{"@ocr-team"},
31+
},
32+
{
33+
name: "No match",
34+
filePath: "core/services/unknown/unknown_test.go",
35+
patterns: []PatternOwner{
36+
{Pattern: "/core/services/job", Owners: []string{"@team1"}}, // Leading /
37+
{Pattern: "/core/services/ocr*", Owners: []string{"@ocr-team"}}, // Leading /
38+
},
39+
expectedOwners: nil,
40+
},
41+
{
42+
name: "Multiple matches, last wins",
43+
filePath: "core/services/ocr_test.go",
44+
patterns: []PatternOwner{
45+
{Pattern: "/core/services/*", Owners: []string{"@general-team"}}, // Leading /
46+
{Pattern: "/core/services/ocr*", Owners: []string{"@ocr-team"}}, // Leading /
47+
},
48+
expectedOwners: []string{"@ocr-team"},
49+
},
50+
{
51+
name: "Directory match",
52+
filePath: "core/services/job/subdir/job_test.go",
53+
patterns: []PatternOwner{
54+
{Pattern: "/core/services/job", Owners: []string{"@team1"}}, // Leading /
55+
},
56+
expectedOwners: []string{"@team1"},
57+
},
58+
{
59+
name: "Leading slash directory match",
60+
filePath: "core/capabilities/compute/compute_test.go",
61+
patterns: []PatternOwner{
62+
{
63+
Pattern: "/core/capabilities/",
64+
Owners: []string{"@smartcontractkit/keystone", "@smartcontractkit/capabilities-team"},
65+
},
66+
},
67+
expectedOwners: []string{"@smartcontractkit/keystone", "@smartcontractkit/capabilities-team"},
68+
},
69+
}
70+
71+
for _, test := range tests {
72+
t.Run(test.name, func(t *testing.T) {
73+
owners := FindOwners(test.filePath, test.patterns)
74+
assert.Equal(t, test.expectedOwners, owners)
75+
})
76+
}
77+
}

tools/flakeguard/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ func init() {
2929
rootCmd.AddCommand(cmd.FindTestsCmd)
3030
rootCmd.AddCommand(cmd.RunTestsCmd)
3131
rootCmd.AddCommand(cmd.AggregateResultsCmd)
32+
rootCmd.AddCommand(cmd.CheckTestOwnersCmd)
3233
}
3334

3435
func main() {

0 commit comments

Comments
 (0)